diff --git a/api/graphs/api.graphqls b/api/graphs/api.graphqls index 4ceaf619..83b7ac02 100644 --- a/api/graphs/api.graphqls +++ b/api/graphs/api.graphqls @@ -45,6 +45,8 @@ type User { Phone: String PoolYear: String PoolMonth: String + isSwimmer: Boolean! + isMe: Boolean! Nickname: String AvatarURL: String CoverURL: String diff --git a/cmd/api.go b/cmd/api.go index 9381b37f..08379692 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -70,6 +70,9 @@ var apiCmd = &cobra.Command{ }).Handler) router.Use(api.AuthzByPolicyMiddleware) router.Use(api.AuthenticationMiddleware) + if os.Getenv("DEBUG") == "true" { + router.Use(api.LoggingMiddleware) + } if *playgroudActive { router.Handle("/", playground.Handler("GraphQL playground", "/graphql")) diff --git a/internal/api/api.resolvers.go b/internal/api/api.resolvers.go index 560d4a60..97753808 100644 --- a/internal/api/api.resolvers.go +++ b/internal/api/api.resolvers.go @@ -6,6 +6,9 @@ package api import ( "context" "os" + "strconv" + "strings" + "time" apigen "atomys.codes/stud42/internal/api/generated" typesgen "atomys.codes/stud42/internal/api/generated/types" @@ -207,6 +210,22 @@ func (r *queryResolver) InternalGetUser(ctx context.Context, id uuid.UUID) (*gen return r.client.User.Get(ctx, id) } +func (r *userResolver) IsSwimmer(ctx context.Context, obj *generated.User) (bool, error) { + if obj.PoolYear == nil || obj.PoolMonth == nil { + return false, nil + } + + now := time.Now() + return (*obj.PoolYear == strconv.Itoa(now.Year()) && + strings.EqualFold(*obj.PoolMonth, now.Format("January"))), nil +} + +func (r *userResolver) IsMe(ctx context.Context, obj *generated.User) (bool, error) { + cu, _ := CurrentUserFromContext(ctx) + + return cu.ID == obj.ID, nil +} + func (r *userResolver) Flags(ctx context.Context, obj *generated.User) ([]typesgen.Flag, error) { return modelsutils.TranslateFlagFromORM(obj.FlagsList), nil } diff --git a/internal/api/authentication.go b/internal/api/authentication.go index 2496fc6f..623322fa 100644 --- a/internal/api/authentication.go +++ b/internal/api/authentication.go @@ -21,7 +21,7 @@ import ( // authTokenContextKey is the context key to store the JWT Token from the // Authorization header. const authTokenContextKey contextKey = "auth_token" -const currentUsreContextKey contextKey = "auth_current_user" +const currentUserContextKey contextKey = "auth_current_user" // errUnauthenticated is the error returned by the directiveAuthorization // when the request is not authenticated. @@ -80,17 +80,19 @@ func directiveAuthorization(client *modelgen.Client) func(ctx context.Context, o return nil, errors.New("token expired") } - user, err := client.User.Query(). - Where(user.ID(uuid.MustParse(tok.Subject()))). - WithFollowing(). - WithFollowers(). - WithCurrentLocation(). - Only(ctx) - if err != nil { - return nil, errUnauthenticated - } + if ctx.Value(currentUserContextKey) == nil { + user, err := client.User.Query(). + Where(user.ID(uuid.MustParse(tok.Subject()))). + WithFollowing(). + WithFollowers(). + WithCurrentLocation(). + Only(ctx) + if err != nil { + return nil, errUnauthenticated + } - ctx = context.WithValue(ctx, currentUsreContextKey, user) + ctx = context.WithValue(ctx, currentUserContextKey, user) + } return next(ctx) } @@ -98,7 +100,7 @@ func directiveAuthorization(client *modelgen.Client) func(ctx context.Context, o // CurrentUserFromContext will retrieve the current user from the context. func CurrentUserFromContext(ctx context.Context) (*modelgen.User, error) { - user, ok := ctx.Value(currentUsreContextKey).(*modelgen.User) + user, ok := ctx.Value(currentUserContextKey).(*modelgen.User) if !ok { return nil, errUnauthenticated } diff --git a/internal/api/logging.go b/internal/api/logging.go new file mode 100644 index 00000000..2109b031 --- /dev/null +++ b/internal/api/logging.go @@ -0,0 +1,57 @@ +package api + +import ( + "net/http" + "runtime/debug" + "time" + + "github.com/rs/zerolog/log" +) + +// responseWriter is a minimal wrapper for http.ResponseWriter that allows the +// written HTTP status code to be captured for logging. +type responseWriter struct { + http.ResponseWriter + status int + wroteHeader bool +} + +func wrapResponseWriter(w http.ResponseWriter) *responseWriter { + return &responseWriter{ResponseWriter: w} +} + +func (rw *responseWriter) Status() int { + return rw.status +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader { + return + } + + rw.status = code + rw.ResponseWriter.WriteHeader(code) + rw.wroteHeader = true +} + +// LoggingMiddleware logs the incoming HTTP request & its duration. +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Error().Err(err.(error)).Interface("trace", debug.Stack()).Msg("") + } + }() + + start := time.Now() + wrapped := wrapResponseWriter(w) + next.ServeHTTP(wrapped, r) + log.Debug(). + Str("method", r.Method). + Int("status", wrapped.status). + Str("path", r.URL.EscapedPath()). + Dur("duration", time.Since(start)). + Msg("request processed") + }) +} diff --git a/web/ui/src/components/ClusterMap/ClusterMap.tsx b/web/ui/src/components/ClusterMap/ClusterMap.tsx index 9522d933..56036d8f 100644 --- a/web/ui/src/components/ClusterMap/ClusterMap.tsx +++ b/web/ui/src/components/ClusterMap/ClusterMap.tsx @@ -54,8 +54,12 @@ export const ClusterWorkspaceWithUser = ({