started "real work"
This commit is contained in:
@@ -6,10 +6,14 @@ require github.com/go-sql-driver/mysql v1.8.1
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/jfarleyx/go-simple-cache v1.1.0 // indirect
|
github.com/jfarleyx/go-simple-cache v1.1.0 // indirect
|
||||||
github.com/jmoiron/sqlx v1.4.0 // indirect
|
github.com/jmoiron/sqlx v1.4.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
@@ -17,11 +21,14 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.29.0 // indirect
|
||||||
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.23.0
|
||||||
github.com/gofiber/fiber/v2 v2.52.6
|
github.com/gofiber/fiber/v2 v2.52.6
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4
|
|||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||||
|
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
@@ -18,6 +26,8 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
|||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
@@ -42,11 +52,15 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
|||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ import (
|
|||||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
type assignments map[string]string
|
type assignments map[string]*string
|
||||||
|
|
||||||
type assignemntDB struct {
|
type assignemntDB struct {
|
||||||
TaskName string `db:"taskName"`
|
TaskName string `db:"taskName"`
|
||||||
UserName string `db:"userName"`
|
UserName *string `db:"userName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Event(eventID int) (assignments, error) {
|
func Event(eventID int) (assignments, error) {
|
||||||
// get the assignments from the database
|
// get the assignments from the database
|
||||||
var assignmentRows []assignemntDB
|
var assignmentRows []assignemntDB
|
||||||
|
|
||||||
if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, TASKS.text AS taskName FROM USER_ASSIGNMENTS JOIN USERS ON USER_ASSIGNMENTS.userName = USERS.name LEFT JOIN TASKS ON USER_ASSIGNMENTS.taskID = TASKS.id WHERE USER_ASSIGNMENTS.eventID = ?", eventID); err != nil {
|
if err := db.DB.Select(&assignmentRows, "SELECT USERS.name AS userName, TASKS.text AS taskName FROM USER_ASSIGNMENTS LEFT JOIN USERS ON USER_ASSIGNMENTS.userName = USERS.name LEFT JOIN TASKS ON USER_ASSIGNMENTS.taskID = TASKS.id WHERE USER_ASSIGNMENTS.eventID = ?", eventID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
// transform the rows into the returned map
|
// transform the rows into the returned map
|
||||||
|
|||||||
@@ -3,46 +3,70 @@ package events
|
|||||||
import (
|
import (
|
||||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
"github.com/johannesbuehl/golunteer/backend/pkg/db/assignments"
|
"github.com/johannesbuehl/golunteer/backend/pkg/db/assignments"
|
||||||
|
"github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type EventWithAssignment struct {
|
||||||
eventDataDB
|
eventDataDB
|
||||||
Tasks []string
|
Tasks map[string]*string `json:"tasks"`
|
||||||
Assignments map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type eventDataDB struct {
|
type eventDataDB struct {
|
||||||
Id int `db:"id"`
|
Id int `db:"id" json:"id"`
|
||||||
Date string `db:"date"`
|
Date string `db:"date" json:"date"`
|
||||||
Description string `db:"description"`
|
Description string `db:"description" json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// transform the database-entry to an Event
|
// transform the database-entry to an Event
|
||||||
func (e *eventDataDB) Event() (Event, error) {
|
func (e *eventDataDB) Event() (EventWithAssignment, error) {
|
||||||
// get the availabilites associated with the event
|
// get the availabilites associated with the event
|
||||||
if assignemnts, err := assignments.Event(e.Id); err != nil {
|
if assignemnts, err := assignments.Event(e.Id); err != nil {
|
||||||
return Event{}, err
|
return EventWithAssignment{}, err
|
||||||
} else {
|
} else {
|
||||||
return Event{
|
return EventWithAssignment{
|
||||||
eventDataDB: *e,
|
eventDataDB: *e,
|
||||||
Assignments: assignemnts,
|
Tasks: assignemnts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get all the event ids
|
func All() ([]eventDataDB, error) {
|
||||||
func All() (map[int]eventDataDB, error) {
|
|
||||||
var dbRows []eventDataDB
|
var dbRows []eventDataDB
|
||||||
|
|
||||||
if err := db.DB.Select(&dbRows, "SELECT * FROM EVENTS"); err != nil {
|
if err := db.DB.Select(&dbRows, "SELECT *, DATE_FORMAT(date, '%Y-%m-%dT%H:%i:%s') as date FROM EVENTS"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
eventsMap := map[int]eventDataDB{}
|
return dbRows, nil
|
||||||
|
}
|
||||||
for _, idRow := range dbRows {
|
}
|
||||||
eventsMap[idRow.Id] = idRow
|
|
||||||
}
|
func WithAssignments() ([]EventWithAssignment, error) {
|
||||||
|
// get all events
|
||||||
return eventsMap, nil
|
if eventsDB, err := All(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
events := make([]EventWithAssignment, len(eventsDB))
|
||||||
|
|
||||||
|
for ii, e := range eventsDB {
|
||||||
|
if ev, err := e.Event(); err != nil {
|
||||||
|
logger.Logger.Error().Msgf("can't get assignments for event with id = %d: %v", e.Id, err)
|
||||||
|
} else {
|
||||||
|
events[ii] = ev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserPending(userName string) (int, error) {
|
||||||
|
var result struct {
|
||||||
|
Count int `db:"count(*)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.QueryRowx("SELECT count(*) FROM USERS WHERE name = ? AND name NOT IN (SELECT userName FROM USER_AVAILABILITIES)", userName).StructScan(&result); err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return result.Count, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,29 @@ import (
|
|||||||
"github.com/johannesbuehl/golunteer/backend/pkg/db/events"
|
"github.com/johannesbuehl/golunteer/backend/pkg/db/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getEvents(c *fiber.Ctx) responseMessage {
|
func getEventsAssignments(args HandlerArgs) responseMessage {
|
||||||
response := responseMessage{}
|
response := responseMessage{}
|
||||||
|
|
||||||
// get all eventRows
|
if events, err := events.WithAssignments(); err != nil {
|
||||||
if eventRows, err := events.All(); err != nil {
|
|
||||||
response.Status = fiber.StatusInternalServerError
|
response.Status = fiber.StatusInternalServerError
|
||||||
|
|
||||||
logger.Error().Msgf("events retrieving failed: %v", err)
|
logger.Error().Msgf("can't retrieve events with assignments: %v", err)
|
||||||
} else {
|
} else {
|
||||||
// get the data for all the allEvents
|
response.Data = events
|
||||||
allEvents := []events.Event{}
|
}
|
||||||
|
|
||||||
for _, eventRow := range eventRows {
|
return response
|
||||||
if e, err := eventRow.Event(); err != nil {
|
}
|
||||||
logger.Error().Msgf("error while populating event with id = %d: %v", eventRow.Id, err)
|
|
||||||
} else {
|
func getEventsUserPending(args HandlerArgs) responseMessage {
|
||||||
allEvents = append(allEvents, e)
|
response := responseMessage{}
|
||||||
}
|
|
||||||
|
if count, err := events.UserPending(args.User.UserName); err != nil {
|
||||||
// response.Data = struct{ Events []events.Event }{Events: allEvents}
|
response.Status = fiber.StatusInternalServerError
|
||||||
response.Data = allEvents
|
|
||||||
}
|
logger.Warn().Msgf("can't query database for users %q pending events: %v", args.User.UserName, err)
|
||||||
|
} else {
|
||||||
|
response.Data = count
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,67 +1,98 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
"github.com/johannesbuehl/golunteer/backend/pkg/db"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserLogin struct {
|
|
||||||
UserName string `json:"userName"`
|
|
||||||
LoggedIn bool `json:"loggedIn"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle welcome-messages from clients
|
// handle welcome-messages from clients
|
||||||
func handleWelcome(c *fiber.Ctx) error {
|
func handleWelcome(c *fiber.Ctx) error {
|
||||||
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||||
|
|
||||||
response := responseMessage{}
|
response := responseMessage{}
|
||||||
response.Data = UserLogin{
|
response.Data = UserChecked{
|
||||||
LoggedIn: false,
|
Admin: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ok, err := checkUser(c); err != nil {
|
if user, err := checkUser(c); err != nil {
|
||||||
response.Status = fiber.StatusInternalServerError
|
response.Status = fiber.StatusInternalServerError
|
||||||
|
|
||||||
logger.Warn().Msgf("can't check user: %v", err)
|
logger.Warn().Msgf("can't check user: %v", err)
|
||||||
} else if !ok {
|
} else if user == nil {
|
||||||
response.Status = fiber.StatusNoContent
|
response.Status = fiber.StatusNoContent
|
||||||
|
|
||||||
|
logger.Debug().Msgf("user not authorized")
|
||||||
} else {
|
} else {
|
||||||
if uid, _, err := extractJWT(c); err != nil {
|
response.Data = UserChecked{
|
||||||
response.Status = fiber.StatusBadRequest
|
UserName: user.UserName,
|
||||||
|
Admin: user.Admin,
|
||||||
logger.Error().Msgf("can't extract JWT: %v", err)
|
|
||||||
} else {
|
|
||||||
if users, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", strconv.Itoa(uid)); err != nil {
|
|
||||||
response.Status = fiber.StatusInternalServerError
|
|
||||||
|
|
||||||
logger.Error().Msgf("can't get users from database: %v", err)
|
|
||||||
} else {
|
|
||||||
if len(users) != 1 {
|
|
||||||
response.Status = fiber.StatusForbidden
|
|
||||||
response.Message = "unknown user"
|
|
||||||
|
|
||||||
removeSessionCookie(c)
|
|
||||||
} else {
|
|
||||||
user := users[0]
|
|
||||||
|
|
||||||
response.Data = UserLogin{
|
|
||||||
UserName: user.UserName,
|
|
||||||
LoggedIn: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debug().Msgf("welcomed user with uid = %v", uid)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug().Msgf("welcomed user %q", user.UserName)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.send(c)
|
return response.send(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const messageWrongLogin = "Unkown user or wrong password"
|
||||||
|
|
||||||
func handleLogin(c *fiber.Ctx) error {
|
func handleLogin(c *fiber.Ctx) error {
|
||||||
panic("not implemented yet")
|
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||||
|
|
||||||
|
// extract username and password from the request
|
||||||
|
requestBody := struct {
|
||||||
|
Username string `json:"userName" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
var response responseMessage
|
||||||
|
|
||||||
|
if err := c.BodyParser(&requestBody); err != nil {
|
||||||
|
logger.Debug().Msgf("can't parse login-body: %v", err)
|
||||||
|
|
||||||
|
response.Status = fiber.StatusBadRequest
|
||||||
|
|
||||||
|
// validate the body
|
||||||
|
} else if err := validate.Struct(requestBody); err != nil {
|
||||||
|
logger.Warn().Msgf("can't parse login-body: %v", err)
|
||||||
|
} else {
|
||||||
|
// query the database for the user
|
||||||
|
var result userDB
|
||||||
|
if err := db.DB.QueryRowx("SELECT password, admin, tokenID FROM USERS WHERE name = ?", requestBody.Username).StructScan(&result); err != nil {
|
||||||
|
response.Status = fiber.StatusForbidden
|
||||||
|
response.Message = messageWrongLogin
|
||||||
|
|
||||||
|
logger.Info().Msgf("can't get user with name = %q from database", requestBody.Username)
|
||||||
|
} else {
|
||||||
|
// hash the password
|
||||||
|
if bcrypt.CompareHashAndPassword(result.Password, []byte(requestBody.Password)) != nil {
|
||||||
|
response.Status = fiber.StatusForbidden
|
||||||
|
|
||||||
|
logger.Info().Msgf("login denied: wrong password for user with name = %q", requestBody.Username)
|
||||||
|
} else {
|
||||||
|
// password is correct -> generate the JWT
|
||||||
|
if jwt, err := config.SignJWT(JWTPayload{
|
||||||
|
UserID: requestBody.Username,
|
||||||
|
TokenID: result.TokenID,
|
||||||
|
}); err != nil {
|
||||||
|
response.Status = fiber.StatusInternalServerError
|
||||||
|
logger.Error().Msgf("can't create JWT: %v", err)
|
||||||
|
} else {
|
||||||
|
setSessionCookie(c, &jwt)
|
||||||
|
|
||||||
|
response.Data = UserChecked{
|
||||||
|
UserName: requestBody.Username,
|
||||||
|
Admin: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Msgf("user %q logged in", requestBody.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.send(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handles logout-requests
|
// handles logout-requests
|
||||||
@@ -70,9 +101,5 @@ func handleLogout(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
removeSessionCookie(c)
|
removeSessionCookie(c)
|
||||||
|
|
||||||
return responseMessage{
|
return responseMessage{}.send(c)
|
||||||
Data: UserLogin{
|
|
||||||
LoggedIn: false,
|
|
||||||
},
|
|
||||||
}.send(c)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
|
||||||
@@ -11,6 +12,8 @@ import (
|
|||||||
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var validate *validator.Validate
|
||||||
|
|
||||||
var logger = _logger.Logger
|
var logger = _logger.Logger
|
||||||
var config = _config.Config
|
var config = _config.Config
|
||||||
|
|
||||||
@@ -47,7 +50,14 @@ func (result responseMessage) send(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HandlerArgs struct {
|
||||||
|
C *fiber.Ctx
|
||||||
|
User UserChecked
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
validate = validator.New()
|
||||||
|
|
||||||
// setup fiber
|
// setup fiber
|
||||||
app = fiber.New(fiber.Config{
|
app = fiber.New(fiber.Config{
|
||||||
AppName: "johannes-pv",
|
AppName: "johannes-pv",
|
||||||
@@ -63,8 +73,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// map with the individual registered endpoints
|
// map with the individual registered endpoints
|
||||||
endpoints := map[string]map[string]func(*fiber.Ctx) responseMessage{
|
endpoints := map[string]map[string]func(HandlerArgs) responseMessage{
|
||||||
"GET": {"events": getEvents},
|
"GET": {"events/assignments": getEventsAssignments, "events/user/pending": getEventsUserPending},
|
||||||
"POST": {},
|
"POST": {},
|
||||||
"PATCH": {},
|
"PATCH": {},
|
||||||
"DELETE": {},
|
"DELETE": {},
|
||||||
@@ -81,7 +91,28 @@ func init() {
|
|||||||
handleMethods[method]("/api/"+address, func(c *fiber.Ctx) error {
|
handleMethods[method]("/api/"+address, func(c *fiber.Ctx) error {
|
||||||
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())
|
||||||
|
|
||||||
return handler(c).send(c)
|
var response responseMessage
|
||||||
|
|
||||||
|
if user, err := checkUser(c); err != nil {
|
||||||
|
response = responseMessage{
|
||||||
|
Status: fiber.StatusBadRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error().Msgf("can't check user: %v", err)
|
||||||
|
} else if user == nil {
|
||||||
|
response = responseMessage{
|
||||||
|
Status: fiber.StatusNoContent,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log().Msgf("user not authorized")
|
||||||
|
} else {
|
||||||
|
response = handler(HandlerArgs{
|
||||||
|
C: c,
|
||||||
|
User: *user,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.send(c)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -125,7 +156,7 @@ func removeSessionCookie(c *fiber.Ctx) {
|
|||||||
|
|
||||||
// payload of the JSON webtoken
|
// payload of the JSON webtoken
|
||||||
type JWTPayload struct {
|
type JWTPayload struct {
|
||||||
UserID int `json:"userID"`
|
UserID string `json:"userID"`
|
||||||
TokenID string `json:"tokenID"`
|
TokenID string `json:"tokenID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +169,7 @@ type JWT struct {
|
|||||||
// extracts the json webtoken from the request
|
// extracts the json webtoken from the request
|
||||||
//
|
//
|
||||||
// @returns (userID, tokenID, error)
|
// @returns (userID, tokenID, error)
|
||||||
func extractJWT(c *fiber.Ctx) (int, string, error) {
|
func extractJWT(c *fiber.Ctx) (string, string, error) {
|
||||||
// get the session-cookie
|
// get the session-cookie
|
||||||
cookie := c.Cookies("session")
|
cookie := c.Cookies("session")
|
||||||
|
|
||||||
@@ -151,70 +182,57 @@ func extractJWT(c *fiber.Ctx) (int, string, error) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return -1, "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the claims from the JWT
|
// extract the claims from the JWT
|
||||||
if claims, ok := token.Claims.(*JWT); ok && token.Valid {
|
if claims, ok := token.Claims.(*JWT); ok && token.Valid {
|
||||||
return claims.CustomClaims.UserID, claims.CustomClaims.TokenID, nil
|
return claims.CustomClaims.UserID, claims.CustomClaims.TokenID, nil
|
||||||
} else {
|
} else {
|
||||||
return -1, "", fmt.Errorf("invalid JWT")
|
return "", "", fmt.Errorf("invalid JWT")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// user-entry in the database
|
// user-entry in the database
|
||||||
type UserDB struct {
|
type userDB struct {
|
||||||
UserName string `json:"userName"`
|
UserName string `db:"userName"`
|
||||||
Password []byte `json:"password"`
|
Password []byte `db:"password"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `db:"admin"`
|
||||||
TokenID string `json:"tokenID"`
|
TokenID string `db:"tokenID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserChecked struct {
|
||||||
|
UserName string `json:"userName" db:"userName"`
|
||||||
|
Admin bool `json:"admin" db:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks wether the request is from a valid user
|
// checks wether the request is from a valid user
|
||||||
func checkUser(c *fiber.Ctx) (bool, error) {
|
func checkUser(c *fiber.Ctx) (*UserChecked, error) {
|
||||||
uid, tid, err := extractJWT(c)
|
userName, tokenID, err := extractJWT(c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var dbResult struct {
|
||||||
|
TokenID string `db:"tokenID"`
|
||||||
|
Admin bool `db:"admin"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the user from the database
|
// retrieve the user from the database
|
||||||
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
|
if err := db.DB.QueryRowx("SELECT tokenID, admin FROM USERS WHERE name = ?", userName).StructScan(&dbResult); err != nil {
|
||||||
|
return nil, err
|
||||||
|
|
||||||
if err != nil {
|
// if the tokenID is valid, the user is authorized
|
||||||
return false, err
|
} else if dbResult.TokenID != tokenID {
|
||||||
}
|
return nil, err
|
||||||
|
} else {
|
||||||
// if exactly one user came back and the tID is valid, the user is authorized
|
|
||||||
if len(response) == 1 && response[0].TokenID == tid {
|
|
||||||
// reset the expiration of the cookie
|
// reset the expiration of the cookie
|
||||||
setSessionCookie(c, nil)
|
setSessionCookie(c, nil)
|
||||||
|
|
||||||
return true, err
|
return &UserChecked{
|
||||||
} else {
|
UserName: userName,
|
||||||
return false, err
|
Admin: dbResult.Admin,
|
||||||
}
|
}, err
|
||||||
}
|
|
||||||
|
|
||||||
// checks wether the request is from the admin
|
|
||||||
func checkAdmin(c *fiber.Ctx) (bool, error) {
|
|
||||||
uid, tokenID, err := extractJWT(c)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve the user from the database
|
|
||||||
response, err := db.SelectOld[UserDB]("users", "uid = ? LIMIT 1", uid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// if exactly one user came back and its name is "admin", the user is the admin
|
|
||||||
if len(response) != 1 {
|
|
||||||
return false, fmt.Errorf("user doesn't exist")
|
|
||||||
} else {
|
|
||||||
return response[0].UserName == "admin" && response[0].TokenID == tokenID, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
// output: "export",
|
output: "export",
|
||||||
|
|
||||||
rewrites: async () => ({
|
rewrites: async () => ({
|
||||||
beforeFiles: [],
|
beforeFiles: [],
|
||||||
afterFiles: [],
|
afterFiles: [],
|
||||||
|
|||||||
17301
client/package-lock.json
generated
17301
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "client",
|
"name": "client",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@carbon/icons-react": "^11.53.0",
|
"@carbon/icons-react": "^11.53.0",
|
||||||
"@internationalized/date": "^3.6.0",
|
"@internationalized/date": "^3.6.0",
|
||||||
"@nextui-org/button": "^2.2.9",
|
"@nextui-org/react": "^2.6.11",
|
||||||
"@nextui-org/card": "^2.2.9",
|
"@nextui-org/system": "^2.4.6",
|
||||||
"@nextui-org/checkbox": "^2.3.8",
|
"@nextui-org/theme": "^2.4.5",
|
||||||
"@nextui-org/date-picker": "^2.3.9",
|
"@react-aria/i18n": "^3.12.4",
|
||||||
"@nextui-org/divider": "^2.2.5",
|
"@react-stately/data": "^3.12.0",
|
||||||
"@nextui-org/form": "^2.1.8",
|
"framer-motion": "^11.15.0",
|
||||||
"@nextui-org/input": "^2.4.8",
|
"next": "15.1.3",
|
||||||
"@nextui-org/link": "^2.2.7",
|
"react": "^19.0.0",
|
||||||
"@nextui-org/modal": "^2.2.7",
|
"react-dom": "^19.0.0",
|
||||||
"@nextui-org/radio": "^2.3.8",
|
"zustand": "^5.0.2"
|
||||||
"@nextui-org/select": "^2.4.9",
|
},
|
||||||
"@nextui-org/spinner": "^2.2.6",
|
"devDependencies": {
|
||||||
"@nextui-org/switch": "^2.2.8",
|
"@eslint/eslintrc": "^3",
|
||||||
"@nextui-org/system": "^2.4.6",
|
"@types/node": "^20",
|
||||||
"@nextui-org/table": "^2.2.8",
|
"@types/react": "^19",
|
||||||
"@nextui-org/theme": "^2.4.5",
|
"@types/react-dom": "^19",
|
||||||
"@nextui-org/tooltip": "^2.2.7",
|
"eslint": "^9",
|
||||||
"@react-aria/i18n": "^3.12.4",
|
"eslint-config-next": "15.1.3",
|
||||||
"@react-stately/data": "^3.12.0",
|
"postcss": "^8",
|
||||||
"framer-motion": "^11.15.0",
|
"prettier": "^3.4.2",
|
||||||
"next": "15.1.3",
|
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||||
"react": "^19.0.0",
|
"tailwindcss": "^3.4.1",
|
||||||
"react-dom": "^19.0.0",
|
"typescript": "^5"
|
||||||
"zustand": "^5.0.2"
|
}
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@eslint/eslintrc": "^3",
|
|
||||||
"@types/node": "^20",
|
|
||||||
"@types/react": "^19",
|
|
||||||
"@types/react-dom": "^19",
|
|
||||||
"eslint": "^9",
|
|
||||||
"eslint-config-next": "15.1.3",
|
|
||||||
"postcss": "^8",
|
|
||||||
"prettier": "^3.4.2",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"typescript": "^5"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { DateFormatter as IntlDateFormatter } from "@internationalized/date";
|
import { DateFormatter as IntlDateFormatter } from "@internationalized/date";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
export type Task = string;
|
export type Task = string;
|
||||||
|
|
||||||
@@ -19,43 +22,48 @@ export interface EventData {
|
|||||||
id: number;
|
id: number;
|
||||||
date: string;
|
date: string;
|
||||||
tasks: Partial<Record<Task, string | undefined>>;
|
tasks: Partial<Record<Task, string | undefined>>;
|
||||||
volunteers: Partial<Record<string, Availability>>;
|
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Zustand {
|
interface Zustand {
|
||||||
events: EventData[];
|
events: EventData[];
|
||||||
addEvent: (event: EventData) => void;
|
pendingEvents: number;
|
||||||
|
user: {
|
||||||
|
userName: string;
|
||||||
|
admin: boolean;
|
||||||
|
} | null;
|
||||||
|
setEvents: (events: EventData[]) => void;
|
||||||
|
reset: (zustand?: Partial<Zustand>) => void;
|
||||||
|
setPendingEvents: (c: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const zustand = create<Zustand>()((set) => ({
|
const initialState = {
|
||||||
events: [
|
events: [],
|
||||||
|
user: null,
|
||||||
|
pendingEvents: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const zustand = create<Zustand>()(
|
||||||
|
persist(
|
||||||
|
(set) => ({
|
||||||
|
...initialState,
|
||||||
|
setEvents: (events) => set({ events }),
|
||||||
|
reset: (newZustand) =>
|
||||||
|
set({
|
||||||
|
...initialState,
|
||||||
|
...newZustand,
|
||||||
|
}),
|
||||||
|
setPendingEvents: (c) => set(() => ({ pendingEvents: c })),
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
id: 0,
|
name: "golunteer-storage",
|
||||||
// date: parseDateTime("2025-01-05T11:00[Europe/Berlin]").toString(),
|
partialize: (state) =>
|
||||||
date: "2025-01-05T11:00[Europe/Berlin]",
|
Object.fromEntries(
|
||||||
tasks: {
|
Object.entries(state).filter(([key]) => !["events"].includes(key)),
|
||||||
Audio: "Mark",
|
),
|
||||||
Livestream: undefined,
|
|
||||||
"Stream Audio": undefined,
|
|
||||||
},
|
|
||||||
volunteers: { Mark: "yes", Simon: "maybe", Sophie: "no" },
|
|
||||||
description: "neuer Prädikant",
|
|
||||||
},
|
},
|
||||||
{
|
),
|
||||||
id: 1,
|
);
|
||||||
date: "2025-01-12T11:00[Europe/Berlin]",
|
|
||||||
tasks: {
|
|
||||||
Audio: "Mark",
|
|
||||||
Livestream: undefined,
|
|
||||||
},
|
|
||||||
volunteers: { Mark: "yes", Simon: "maybe" },
|
|
||||||
description: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
addEvent: (event: EventData) =>
|
|
||||||
set((state) => ({ events: state.events.toSpliced(-1, 0, event) })),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export class DateFormatter {
|
export class DateFormatter {
|
||||||
private formatter;
|
private formatter;
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Divider } from "@nextui-org/divider";
|
|
||||||
import { Link } from "@nextui-org/link";
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { SiteLink } from "./layout";
|
||||||
|
import { Divider, Link } from "@nextui-org/react";
|
||||||
|
|
||||||
export default function Footer({
|
export default function Footer({ sites }: { sites: SiteLink[] }) {
|
||||||
sites,
|
|
||||||
}: {
|
|
||||||
sites: { href: string; text: string }[];
|
|
||||||
}) {
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,11 +1,124 @@
|
|||||||
import { Link } from "@nextui-org/link";
|
"use client";
|
||||||
|
|
||||||
|
import { apiCall } from "@/lib";
|
||||||
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
|
import { User } from "@carbon/icons-react";
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Dropdown,
|
||||||
|
DropdownItem,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownTrigger,
|
||||||
|
Link,
|
||||||
|
Navbar,
|
||||||
|
NavbarBrand,
|
||||||
|
NavbarContent,
|
||||||
|
NavbarItem,
|
||||||
|
} from "@nextui-org/react";
|
||||||
|
import zustand from "@/Zustand";
|
||||||
|
import { SiteLink } from "./layout";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function Header({ sites }: { sites: SiteLink[] }) {
|
||||||
|
const router = useRouter();
|
||||||
|
const user = zustand((state) => state.user);
|
||||||
|
const pendingEvents = zustand((state) => state.pendingEvents);
|
||||||
|
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
const result = await apiCall("GET", "logout");
|
||||||
|
|
||||||
|
// if the request was successfull, redirect to the login-page
|
||||||
|
if (result.ok) {
|
||||||
|
// clear the zustand
|
||||||
|
zustand.getState().reset();
|
||||||
|
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the pending events for the counter
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const result = await apiCall<{ pendingEvents: number }>(
|
||||||
|
"GET",
|
||||||
|
"events/user/pending",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
const resultJson = await result.json();
|
||||||
|
zustand.getState().setPendingEvents(resultJson);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
export default function Header() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center">
|
<div>
|
||||||
<Link href="/" className="text-center text-8xl">
|
<Navbar maxWidth="full">
|
||||||
<h1 className="font-display-headline">Volunteer Scheduler</h1>
|
<NavbarBrand onClick={() => router.push("/")}>
|
||||||
</Link>
|
<h1 className="font-display-headline text-xl">Golunteer</h1>
|
||||||
|
</NavbarBrand>
|
||||||
|
|
||||||
|
{user !== null ? (
|
||||||
|
<>
|
||||||
|
<NavbarContent justify="center">
|
||||||
|
{sites.map((s) =>
|
||||||
|
// if the site is no admin-site or the user is an admin, render it
|
||||||
|
!s.admin || user.admin ? (
|
||||||
|
<NavbarItem key={s.href} isActive={pathname === s.href}>
|
||||||
|
<Link
|
||||||
|
href={s.href}
|
||||||
|
color={pathname === s.href ? "primary" : "foreground"}
|
||||||
|
>
|
||||||
|
{s.text}
|
||||||
|
</Link>
|
||||||
|
</NavbarItem>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</NavbarContent>
|
||||||
|
|
||||||
|
<NavbarContent justify="end">
|
||||||
|
<Dropdown placement="bottom-end">
|
||||||
|
<Badge
|
||||||
|
content={pendingEvents}
|
||||||
|
color="danger"
|
||||||
|
aria-label={`${pendingEvents} notifications`}
|
||||||
|
>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Avatar isBordered as="button" icon={<User size={32} />} />
|
||||||
|
</DropdownTrigger>
|
||||||
|
</Badge>
|
||||||
|
<DropdownMenu variant="flat">
|
||||||
|
<DropdownItem
|
||||||
|
key="profile"
|
||||||
|
className="h-14 gap-2"
|
||||||
|
textValue={`signed in as ${user.userName}`}
|
||||||
|
>
|
||||||
|
<p>Signed in as</p>
|
||||||
|
<p className="text-primary">{user.userName}</p>
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="account"
|
||||||
|
onPress={() => router.push("/account")}
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="logout"
|
||||||
|
color="danger"
|
||||||
|
onPress={logout}
|
||||||
|
className="text-danger"
|
||||||
|
>
|
||||||
|
Log Out
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
</NavbarContent>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</Navbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,68 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { apiCall } from "@/lib";
|
import { apiCall } from "@/lib";
|
||||||
import { Spinner } from "@nextui-org/spinner";
|
import zustand from "@/Zustand";
|
||||||
|
import { Spinner } from "@nextui-org/react";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
enum AuthState {
|
enum AuthState {
|
||||||
Loading,
|
LoggedIn,
|
||||||
LoginScreen,
|
LoginScreen,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
LoggedIn,
|
Loading,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Main({ children }: { children: React.ReactNode }) {
|
export default function Main({ children }: { children: React.ReactNode }) {
|
||||||
const [status, setStatus] = useState(AuthState.Loading);
|
const [auth, setAuth] = useState(AuthState.Loading);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
if (pathname === "/login") {
|
let loggedIn = false;
|
||||||
setStatus(AuthState.LoginScreen);
|
|
||||||
} else {
|
if (zustand.getState().user === null) {
|
||||||
const welcomeResult = await apiCall<{
|
const welcomeResult = await apiCall<{
|
||||||
userName: string;
|
userName: string;
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
}>("GET", "welcome");
|
}>("GET", "welcome");
|
||||||
|
|
||||||
if (!welcomeResult.ok) {
|
if (welcomeResult.ok) {
|
||||||
router.push("/login");
|
try {
|
||||||
} else {
|
const response = await welcomeResult.json();
|
||||||
const response = await welcomeResult.json();
|
|
||||||
|
|
||||||
if (response.loggedIn) {
|
if (response.userName !== undefined && response.userName !== "") {
|
||||||
setStatus(AuthState.LoggedIn);
|
zustand.getState().reset({ user: response });
|
||||||
} else {
|
|
||||||
setStatus(AuthState.Unauthorized);
|
loggedIn = true;
|
||||||
}
|
}
|
||||||
|
} catch {}
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loggedIn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathname === "/login") {
|
||||||
|
if (loggedIn) {
|
||||||
|
router.push("/");
|
||||||
|
} else {
|
||||||
|
setAuth(AuthState.LoginScreen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (loggedIn) {
|
||||||
|
setAuth(AuthState.LoggedIn);
|
||||||
|
} else {
|
||||||
|
setAuth(AuthState.Unauthorized);
|
||||||
|
router.push("/login");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
});
|
}, [pathname, router]);
|
||||||
|
|
||||||
switch (status) {
|
switch (auth) {
|
||||||
case AuthState.Loading:
|
case AuthState.Loading:
|
||||||
return <Spinner label="Loading..." />;
|
return <Spinner label="Loading..." />;
|
||||||
case AuthState.LoggedIn:
|
case AuthState.LoggedIn:
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import Event from "../components/Event/Event";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import AddEvent from "../components/Event/AddEvent";
|
import AddEvent from "../components/Event/AddEvent";
|
||||||
import zustand from "../Zustand";
|
import zustand from "../Zustand";
|
||||||
import { Button } from "@nextui-org/button";
|
|
||||||
import AssignmentTable from "@/components/Event/AssignmentTable";
|
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||||
import { useAsyncList } from "@react-stately/data";
|
import { useAsyncList } from "@react-stately/data";
|
||||||
import { apiCall } from "@/lib";
|
import { apiCall } from "@/lib";
|
||||||
|
import { Button } from "@nextui-org/react";
|
||||||
|
|
||||||
export default function EventVolunteer() {
|
export default function EventVolunteer() {
|
||||||
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||||
@@ -18,8 +18,6 @@ export default function EventVolunteer() {
|
|||||||
load: async () => {
|
load: async () => {
|
||||||
const data = await apiCall("GET", "events");
|
const data = await apiCall("GET", "events");
|
||||||
|
|
||||||
console.debug(await data.json());
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: [],
|
items: [],
|
||||||
};
|
};
|
||||||
@@ -27,7 +25,7 @@ export default function EventVolunteer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 p-4">
|
<div className="relative flex-1">
|
||||||
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
<h2 className="mb-4 text-center text-4xl">Overview</h2>
|
||||||
<div className="flex flex-wrap justify-center gap-4">
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
{zustand.getState().events.map((ee) => (
|
{zustand.getState().events.map((ee) => (
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ import AddEvent from "@/components/Event/AddEvent";
|
|||||||
import LocalDate from "@/components/LocalDate";
|
import LocalDate from "@/components/LocalDate";
|
||||||
import zustand, { Availability, EventData, Task, Tasks } from "@/Zustand";
|
import zustand, { Availability, EventData, Task, Tasks } from "@/Zustand";
|
||||||
import { Add, Copy, Edit, TrashCan } from "@carbon/icons-react";
|
import { Add, Copy, Edit, TrashCan } from "@carbon/icons-react";
|
||||||
import { Button, ButtonGroup } from "@nextui-org/button";
|
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
} from "@nextui-org/modal";
|
Select,
|
||||||
import { Select, SelectItem } from "@nextui-org/select";
|
SelectItem,
|
||||||
import {
|
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
TableCell,
|
TableCell,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@nextui-org/table";
|
Tooltip,
|
||||||
import { Tooltip } from "@nextui-org/tooltip";
|
} from "@nextui-org/react";
|
||||||
import { useAsyncList } from "@react-stately/data";
|
import { useAsyncList } from "@react-stately/data";
|
||||||
import React, { Key, useState } from "react";
|
import React, { Key, useState } from "react";
|
||||||
|
|
||||||
|
|||||||
64
client/src/app/events/page.tsx
Normal file
64
client/src/app/events/page.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
import AddEvent from "@/components/Event/AddEvent";
|
||||||
|
import AssignmentTable from "@/components/Event/AssignmentTable";
|
||||||
|
import Event from "@/components/Event/Event";
|
||||||
|
import { apiCall } from "@/lib";
|
||||||
|
import zustand, { EventData } from "@/Zustand";
|
||||||
|
import { Add } from "@carbon/icons-react";
|
||||||
|
import { Button } from "@nextui-org/react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export default function Events() {
|
||||||
|
const [showAddItemDialogue, setShowAddItemDialogue] = useState(false);
|
||||||
|
const events = zustand((state) => state.events);
|
||||||
|
const admin = zustand((state) => state.user?.admin);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
console.debug("query");
|
||||||
|
|
||||||
|
const data = await apiCall<EventData[]>("GET", "events/assignments");
|
||||||
|
|
||||||
|
if (data.ok) {
|
||||||
|
zustand.getState().setEvents(await data.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<h2 className="mb-4 text-center text-4xl">Upcoming Events</h2>
|
||||||
|
<div className="flex flex-wrap justify-center gap-4">
|
||||||
|
{events.map((ee, ii) => (
|
||||||
|
<Event key={ii} event={ee}>
|
||||||
|
<AssignmentTable tasks={ee.tasks} />
|
||||||
|
</Event>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{admin ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
isIconOnly
|
||||||
|
radius="full"
|
||||||
|
className="absolute bottom-0 right-0"
|
||||||
|
onPress={() => setShowAddItemDialogue(true)}
|
||||||
|
>
|
||||||
|
<Add size={32} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<AddEvent
|
||||||
|
className="border-2 border-accent-3"
|
||||||
|
isOpen={showAddItemDialogue}
|
||||||
|
onOpenChange={setShowAddItemDialogue}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,12 +11,48 @@ export const metadata: Metadata = {
|
|||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface SiteLink {
|
||||||
|
text: string;
|
||||||
|
href: string;
|
||||||
|
admin?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const footerSites = [
|
const headerSites: SiteLink[] = [
|
||||||
|
{
|
||||||
|
text: "Overview",
|
||||||
|
href: "/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Events",
|
||||||
|
href: "/events",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Assignments",
|
||||||
|
href: "/assignments",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Assign Tasks",
|
||||||
|
href: "/admin/assign",
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Users",
|
||||||
|
href: "/admin/users",
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Configuration",
|
||||||
|
href: "/admin/config",
|
||||||
|
admin: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const footerSites: SiteLink[] = [
|
||||||
{
|
{
|
||||||
text: "Impressum",
|
text: "Impressum",
|
||||||
href: "/impressum",
|
href: "/impressum",
|
||||||
@@ -33,9 +69,9 @@ export default function RootLayout({
|
|||||||
<NextUIProvider>
|
<NextUIProvider>
|
||||||
<div className="flex min-h-screen flex-col p-4">
|
<div className="flex min-h-screen flex-col p-4">
|
||||||
<header>
|
<header>
|
||||||
<Header />
|
<Header sites={headerSites} />
|
||||||
</header>
|
</header>
|
||||||
<main className="flex min-h-full flex-1 flex-col">
|
<main className="flex min-h-full flex-1 flex-col p-4">
|
||||||
<Main>{children}</Main>
|
<Main>{children}</Main>
|
||||||
</main>
|
</main>
|
||||||
<footer className="flex h-4 justify-center gap-4">
|
<footer className="flex h-4 justify-center gap-4">
|
||||||
|
|||||||
@@ -1,22 +1,49 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ViewFilled, ViewOffFilled } from "@carbon/icons-react";
|
import CheckboxIcon from "@/components/CheckboxIcon";
|
||||||
import { Button } from "@nextui-org/button";
|
import { apiCall } from "@/lib";
|
||||||
import { Form } from "@nextui-org/form";
|
import zustand from "@/Zustand";
|
||||||
import { Input } from "@nextui-org/input";
|
import {
|
||||||
import { Switch } from "@nextui-org/switch";
|
ViewFilled,
|
||||||
import { useState } from "react";
|
ViewOffFilled,
|
||||||
|
WarningHexFilled,
|
||||||
|
} from "@carbon/icons-react";
|
||||||
|
import { Alert, Button, Form, Input } from "@nextui-org/react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
const [wrongPassword, setWrongPassword] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// set login-request
|
||||||
|
async function sendLogin(e: FormEvent<HTMLFormElement>) {
|
||||||
|
const data = Object.fromEntries(new FormData(e.currentTarget));
|
||||||
|
|
||||||
|
const result = await apiCall("POST", "login", undefined, data);
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
// add the user-info to the zustand
|
||||||
|
zustand.getState().reset({ user: await result.json() });
|
||||||
|
|
||||||
|
// redirect to the home-page
|
||||||
|
router.push("/");
|
||||||
|
} else {
|
||||||
|
setWrongPassword(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mb-4 text-center text-4xl">Login</h2>
|
<h2 className="mb-4 text-center text-4xl">Login</h2>
|
||||||
<Form
|
<Form
|
||||||
validationBehavior="native"
|
validationBehavior="native"
|
||||||
className="flex flex-col items-center gap-2"
|
className="mx-auto flex max-w-sm flex-col items-center gap-2"
|
||||||
onSubmit={(e) => e.preventDefault()}
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
void sendLogin(e);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
isRequired
|
isRequired
|
||||||
@@ -24,7 +51,6 @@ export default function Login() {
|
|||||||
label="Name"
|
label="Name"
|
||||||
name="username"
|
name="username"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
className="max-w-xs"
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
isRequired
|
isRequired
|
||||||
@@ -32,7 +58,7 @@ export default function Login() {
|
|||||||
name="password"
|
name="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
endContent={
|
endContent={
|
||||||
<Switch
|
<CheckboxIcon
|
||||||
className="my-auto"
|
className="my-auto"
|
||||||
startContent={<ViewFilled />}
|
startContent={<ViewFilled />}
|
||||||
endContent={<ViewOffFilled />}
|
endContent={<ViewOffFilled />}
|
||||||
@@ -42,9 +68,18 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
type={visibility ? "text" : "password"}
|
type={visibility ? "text" : "password"}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
className="max-w-xs"
|
|
||||||
/>
|
/>
|
||||||
<Button className="w-full max-w-xs" color="primary" type="submit">
|
<Alert
|
||||||
|
title="Login failed"
|
||||||
|
description="Wrong username or password"
|
||||||
|
color="danger"
|
||||||
|
icon={<WarningHexFilled size={32} />}
|
||||||
|
hideIconWrapper
|
||||||
|
isClosable
|
||||||
|
isVisible={wrongPassword}
|
||||||
|
onVisibleChange={(v) => setWrongPassword(v)}
|
||||||
|
/>
|
||||||
|
<Button className="w-full" color="primary" type="submit">
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import Event from "@/components/Event/Event";
|
|
||||||
import zustand, { Availabilities, Availability } from "@/Zustand";
|
|
||||||
import { Radio, RadioGroup } from "@nextui-org/radio";
|
|
||||||
|
|
||||||
function availability2Color(availability: Availability) {
|
|
||||||
switch (availability) {
|
|
||||||
case "yes":
|
|
||||||
return "success";
|
|
||||||
case "maybe":
|
|
||||||
return "warning";
|
|
||||||
default:
|
|
||||||
return "danger";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function OverviewPersonal() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h2 className="mb-4 text-center text-4xl">Upcoming Events</h2>
|
|
||||||
<div className="flex flex-wrap justify-center gap-4">
|
|
||||||
{zustand.getState().events.map((ev) => (
|
|
||||||
<Event key={ev.id} event={ev}>
|
|
||||||
<RadioGroup className="mt-auto" orientation="horizontal">
|
|
||||||
{Availabilities.map((availability) => (
|
|
||||||
<Radio
|
|
||||||
value={availability}
|
|
||||||
key={availability}
|
|
||||||
color={availability2Color(availability)}
|
|
||||||
classNames={{
|
|
||||||
label: `text-${availability2Color(availability)}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{availability}
|
|
||||||
</Radio>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</Event>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import OverviewPersonal from "./me/page";
|
import EventVolunteer from "./Overview";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
// return <EventVolunteer />;
|
return <EventVolunteer />;
|
||||||
return <OverviewPersonal />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
33
client/src/components/CheckboxIcon.tsx
Normal file
33
client/src/components/CheckboxIcon.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { SwitchProps, useSwitch, VisuallyHidden } from "@nextui-org/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function CheckboxIcon(props: SwitchProps) {
|
||||||
|
const {
|
||||||
|
Component,
|
||||||
|
slots,
|
||||||
|
isSelected,
|
||||||
|
getBaseProps,
|
||||||
|
getInputProps,
|
||||||
|
getWrapperProps,
|
||||||
|
} = useSwitch(props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component {...getBaseProps()}>
|
||||||
|
<VisuallyHidden>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
</VisuallyHidden>
|
||||||
|
<div
|
||||||
|
{...getWrapperProps()}
|
||||||
|
className={slots.wrapper({
|
||||||
|
class: [
|
||||||
|
"h-8 w-8",
|
||||||
|
"flex items-center justify-center",
|
||||||
|
"rounded-lg bg-default-100 hover:bg-default-200",
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isSelected ? props.startContent : props.endContent}
|
||||||
|
</div>
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Add } from "@carbon/icons-react";
|
import { Add } from "@carbon/icons-react";
|
||||||
import zustand, { EventData, Task, Tasks } from "../../Zustand";
|
import zustand, { EventData, Task, Tasks } from "../../Zustand";
|
||||||
import { Button } from "@nextui-org/button";
|
|
||||||
import { Checkbox, CheckboxGroup } from "@nextui-org/checkbox";
|
|
||||||
import { DatePicker } from "@nextui-org/date-picker";
|
|
||||||
import { getLocalTimeZone, now, ZonedDateTime } from "@internationalized/date";
|
import { getLocalTimeZone, now, ZonedDateTime } from "@internationalized/date";
|
||||||
import { Textarea } from "@nextui-org/input";
|
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
CheckboxGroup,
|
||||||
|
DatePicker,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
ModalFooter,
|
ModalFooter,
|
||||||
ModalHeader,
|
ModalHeader,
|
||||||
} from "@nextui-org/modal";
|
Textarea,
|
||||||
|
} from "@nextui-org/react";
|
||||||
|
|
||||||
interface state {
|
interface state {
|
||||||
date: ZonedDateTime;
|
date: ZonedDateTime;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card, CardBody, CardHeader } from "@nextui-org/card";
|
|
||||||
import { Divider } from "@nextui-org/divider";
|
|
||||||
import LocalDate from "../LocalDate";
|
import LocalDate from "../LocalDate";
|
||||||
import { EventData } from "@/Zustand";
|
import { EventData } from "@/Zustand";
|
||||||
|
import { Card, CardBody, CardHeader, Divider } from "@nextui-org/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function Event({
|
export default function Event({
|
||||||
@@ -27,7 +26,6 @@ export default function Event({
|
|||||||
options={{
|
options={{
|
||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
timeZone: "Europe/Berlin", // TODO: check with actual backend
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{event.date}
|
{event.date}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use local";
|
"use local";
|
||||||
|
|
||||||
import { DateFormatter } from "@/Zustand";
|
import { DateFormatter } from "@/Zustand";
|
||||||
import { parseZonedDateTime } from "@internationalized/date";
|
import { getLocalTimeZone, parseDateTime } from "@internationalized/date";
|
||||||
import { useLocale } from "@react-aria/i18n";
|
import { useLocale } from "@react-aria/i18n";
|
||||||
|
|
||||||
export default function LocalDate(props: {
|
export default function LocalDate(props: {
|
||||||
@@ -13,7 +13,9 @@ export default function LocalDate(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={props.className}>
|
<span className={props.className}>
|
||||||
{formatter.format(parseZonedDateTime(props.children).toDate())}
|
{formatter.format(
|
||||||
|
parseDateTime(props.children).toDate(getLocalTimeZone()),
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
import { nextui } from "@nextui-org/theme";
|
import { nextui } from "@nextui-org/theme";
|
||||||
|
|
||||||
const HIGHLIGHT = "#ff5053";
|
const HIGHLIGHT = "hsl(359,100%,65.7%)"; // #ff5053
|
||||||
const FOREGROUND = "#fef2ff";
|
const FOREGROUND = "hsl(295,100%,97.5%)"; // #fef2ff
|
||||||
const ACCENT1 = "#b2aaff";
|
const ACCENT1 = "hsl(246,100%,83.3%)"; // #b2aaff
|
||||||
const ACCENT2 = "#6a5fdb";
|
const ACCENT2 = "hsl(245,63.3%,61.6%)"; // #6a5fdb
|
||||||
const ACCENT3 = "#261a66";
|
const ACCENT3 = "hsl(249,59.4%,25.1%)"; // #261a66
|
||||||
const ACCENT4 = "#29114c";
|
const ACCENT4 = "hsl(264,63.4%,18.2%)"; // #29114c
|
||||||
const ACCENT5 = "#190b2f";
|
const ACCENT5 = "hsl(263,62.1%,11.4%)"; // #190b2f
|
||||||
const BACKGROUND = "#0f000a";
|
const BACKGROUND = "hsl(320,100%,2.9%)"; // #0f000a
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
@@ -21,20 +21,7 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
highlight: HIGHLIGHT,
|
highlight: HIGHLIGHT,
|
||||||
foreground: {
|
foreground: FOREGROUND,
|
||||||
DEFAULT: FOREGROUND,
|
|
||||||
"50": FOREGROUND,
|
|
||||||
"100": FOREGROUND,
|
|
||||||
"200": FOREGROUND,
|
|
||||||
"300": FOREGROUND,
|
|
||||||
"400": FOREGROUND,
|
|
||||||
"500": FOREGROUND,
|
|
||||||
"600": "#fce8ff",
|
|
||||||
"700": "#fad0fe",
|
|
||||||
"800": "#f8abfc",
|
|
||||||
"900": "#f579f9",
|
|
||||||
"950": "#eb46ef",
|
|
||||||
},
|
|
||||||
"accent-1": ACCENT1,
|
"accent-1": ACCENT1,
|
||||||
"accent-2": ACCENT2,
|
"accent-2": ACCENT2,
|
||||||
"accent-3": ACCENT3,
|
"accent-3": ACCENT3,
|
||||||
@@ -67,45 +54,45 @@ export default {
|
|||||||
// },
|
// },
|
||||||
primary: {
|
primary: {
|
||||||
DEFAULT: ACCENT2,
|
DEFAULT: ACCENT2,
|
||||||
"50": "#39357a",
|
"50": "hsl(244,100%,9.6%)",
|
||||||
"100": "#42399a",
|
"100": "hsl(244,100%,19.2%)",
|
||||||
"200": "#5144be",
|
"200": "hsl(244,100%,28.8%)",
|
||||||
"300": ACCENT2,
|
"300": "hsl(244,100%,38.4%)",
|
||||||
"400": "#6f6ee6",
|
"400": "hsl(244,100%,46.7%)",
|
||||||
"500": "#8b91ee",
|
"500": "hsl(244,92.5%,58.4%)",
|
||||||
"600": "#acb7f5",
|
"600": "hsl(244,92.5%,68.8%)",
|
||||||
"700": "#cbd3fa",
|
"700": "hsl(244,92.5%,79.2%)",
|
||||||
"800": "#e2e7fd",
|
"800": "hsl(244,92.5%,89.6%)",
|
||||||
"900": "#eff3fe",
|
"900": "hsl(245,92.3%,94.9%)",
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
DEFAULT: ACCENT3,
|
DEFAULT: ACCENT3,
|
||||||
"50": "#3b288a",
|
"50": "hsl(249,66.7%,9.4%)",
|
||||||
"100": "#462fa8",
|
"100": "hsl(249,66.7%,18.8%)",
|
||||||
"200": "#5538c9",
|
"200": "hsl(249,66.7%,28.2%)",
|
||||||
"300": "#634add",
|
"300": "hsl(249,66.7%,37.6%)",
|
||||||
"400": "#776ae8",
|
"400": "hsl(249,66.7%,47.1%)",
|
||||||
"500": "#9a95f0",
|
"500": "hsl(249,59.3%,57.6%)",
|
||||||
"600": "#bdbcf6",
|
"600": "hsl(249,59.3%,68.2%)",
|
||||||
"700": "#dadbfa",
|
"700": "hsl(249,59.3%,78.8%)",
|
||||||
"800": "#ebebfc",
|
"800": "hsl(249,59.3%,89.4%)",
|
||||||
"900": "#f4f4fe",
|
"900": "hsl(249,61.5%,94.9%)",
|
||||||
},
|
},
|
||||||
// background: {
|
// background: {
|
||||||
// DEFAULT: BACKGROUND,
|
// DEFAULT: BACKGROUND,
|
||||||
// },
|
// },
|
||||||
danger: {
|
danger: {
|
||||||
DEFAULT: HIGHLIGHT,
|
DEFAULT: HIGHLIGHT,
|
||||||
"50": "#fff1f1",
|
"50": "hsl(360,84.9%,10.4%)",
|
||||||
"100": "#ffe1e2",
|
"100": "hsl(359,86.5%,20.4%)",
|
||||||
"200": "#ffc7c8",
|
"200": "hsl(359,86%,30.8%)",
|
||||||
"300": "#ffa0a2",
|
"300": "hsl(359,86.5%,40.8%)",
|
||||||
"400": HIGHLIGHT,
|
"400": "hsl(359,90.4%,51.2%)",
|
||||||
"500": "#f83b3e",
|
"500": "hsl(359,90%,60.8%)",
|
||||||
"600": "#e51d20",
|
"600": "hsl(359,90.6%,70.8%)",
|
||||||
"700": "#c11417",
|
"700": "hsl(359,90%,80.4%)",
|
||||||
"800": "#a01416",
|
"800": "hsl(360,91.8%,90.4%)",
|
||||||
"900": "#84181a",
|
"900": "hsl(359,92%,95.1%)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.32.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
_config "github.com/johannesbuehl/golunteer/setup/pkg/config"
|
_config "github.com/johannesbuehl/golunteer/setup/pkg/config"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
@@ -44,7 +46,7 @@ func main() {
|
|||||||
DBName: config.Database.Database,
|
DBName: config.Database.Database,
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := sql.Open("mysql", sqlConfig.FormatDSN())
|
db, err := sqlx.Open("mysql", sqlConfig.FormatDSN())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
exit(err)
|
exit(err)
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,8 @@ func main() {
|
|||||||
fmt.Println("\thashed password")
|
fmt.Println("\thashed password")
|
||||||
|
|
||||||
// create an admin-user
|
// create an admin-user
|
||||||
if _, err := db.Exec("INSERT INTO USERS (name, password) VALUES ('admin', ?)", passwordHash); err != nil {
|
tokenId := uuid.NewString()
|
||||||
|
if _, err := db.Exec("INSERT INTO USERS (name, password, tokenID) VALUES ('admin', ?, ?)", passwordHash, tokenId); err != nil {
|
||||||
exit(err)
|
exit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ CREATE TABLE USERS (
|
|||||||
name varchar(64) PRIMARY KEY,
|
name varchar(64) PRIMARY KEY,
|
||||||
password binary(60) NOT NULL,
|
password binary(60) NOT NULL,
|
||||||
admin BOOL NOT NULL DEFAULT(false),
|
admin BOOL NOT NULL DEFAULT(false),
|
||||||
tokenID varchar(64) DEFAULT NULL
|
tokenID varchar(64) NOT NULL,
|
||||||
|
CHECK (CHAR_LENGTH(password) = 60),
|
||||||
|
CHECK (CHAR_LENGTH(tokenID) = 36)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE EVENTS (
|
CREATE TABLE EVENTS (
|
||||||
|
|||||||
Reference in New Issue
Block a user