Skip to content

Commit

Permalink
feat: implement colors for me and swimmer on cluster map (#184)
Browse files Browse the repository at this point in the history
* feat: implement colors for me and swimmer on cluster map

* chore: disable debug logging in production

* fix: typescript TS2349 error on apollo

* refactor: apollo cookie getter is now cached

* chore: ignore typescript due to an issue with nextjs
  • Loading branch information
42atomys authored Jul 20, 2022
1 parent 460dcb4 commit 91e6db1
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 36 deletions.
2 changes: 2 additions & 0 deletions api/graphs/api.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type User {
Phone: String
PoolYear: String
PoolMonth: String
isSwimmer: Boolean!
isMe: Boolean!
Nickname: String
AvatarURL: String
CoverURL: String
Expand Down
3 changes: 3 additions & 0 deletions cmd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
19 changes: 19 additions & 0 deletions internal/api/api.resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down
26 changes: 14 additions & 12 deletions internal/api/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -80,25 +80,27 @@ 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)
}
}

// 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
}
Expand Down
57 changes: 57 additions & 0 deletions internal/api/logging.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
8 changes: 6 additions & 2 deletions web/ui/src/components/ClusterMap/ClusterMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ export const ClusterWorkspaceWithUser = ({
<div
className={classNames(
'flex flex-1 flex-col justify-center items-center m-0.5 rounded text-slate-500',
location.user.isFollowing
? 'cursor-pointer bg-blue-300/30 dark:bg-blue-700/30 text-blue-500'
location.user.isMe
? 'cursor-pointer bg-cyan-300/60 dark:bg-cyan-700/60 text-cyan-500'
: location.user.isFollowing
? 'cursor-pointer bg-blue-300/60 dark:bg-blue-700/60 text-blue-500'
: location.user.isSwimmer
? 'cursor-pointer bg-yellow-300/30 dark:bg-yellow-700/30 text-yellow-500'
: 'cursor-pointer bg-emerald-300/30 dark:bg-emerald-700/30 text-emerald-500'
)}
onClick={(e) => onClick && onClick(e, location)}
Expand Down
14 changes: 8 additions & 6 deletions web/ui/src/components/UserCard/UserCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ export const UserCard: UserCardComponent = ({
<h2 className="font-bold uppercase">{user.duoLogin}</h2>
<Name className="font-light" user={user} />
<LocationBadge location={location} />
<DropdownMenu
userID={user.id}
isFriend={user.isFollowing}
buttonAlwaysShow={buttonAlwaysShow}
refetchQueries={refetchQueries}
/>
{!user.isMe && (
<DropdownMenu
userID={user.id}
isFriend={user.isFollowing}
buttonAlwaysShow={buttonAlwaysShow}
refetchQueries={refetchQueries}
/>
)}
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions web/ui/src/graphql/definitions.gql
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ query clusterView($campusName: String!, $identifierPrefix: String!) {
usualFirstName
lastName
duoAvatarURL
isMe
isSwimmer
isFollowing
}
}
Expand Down
25 changes: 13 additions & 12 deletions web/ui/src/lib/apollo.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
import {
ApolloClient,
from,
ApolloQueryResult,
createHttpLink,
from,
InMemoryCache,
QueryOptions,
ApolloQueryResult,
NetworkStatus,
QueryOptions,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import Cookies from 'js-cookie';
import { NextApiRequestCookies } from 'next/dist/server/api-utils';
import { GetServerSidePropsContext } from 'next';
import { NextRequest } from 'next/server'; // eslint-disable-line

export type ServerSideRequest = {
cookies: NextApiRequestCookies & {
'__s42.auth-token'?: string;
};
};
export type ServerSideRequest = NextRequest | GetServerSidePropsContext['req'];

const tokenCookieName = '__s42.auth-token';

const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_API,
credentials: 'include',
});

const authLink = setContext((_, context) => {
const COOKIE_NAME = '__s42.auth-token';
let authToken = Cookies.get(COOKIE_NAME);
let authToken = Cookies.get(tokenCookieName);

if (!authToken) {
authToken = context.authToken;
Expand Down Expand Up @@ -77,7 +75,10 @@ export const queryAuthenticatedSSR = async <T = any>(
return apolloClient.query<T>({
query,
context: {
authToken: req.cookies['__s42.auth-token'],
// @ts-ignore this will works anytime. a NextJS update breaks this rules
// on NextMiddleware due to the implementation of NextCookies. Wait the
// resolution of typescript type
authToken: req.cookies[tokenCookieName],
...context,
},
...rest,
Expand Down
8 changes: 4 additions & 4 deletions web/ui/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import GraphQLAdapter from '@lib/GraphqlAdapter';
import { decodeJWT, encodeJWT } from '@lib/jwt';
import NextAuth, { Account, Profile, User } from 'next-auth';
import FortyTwoProvider from 'next-auth/providers/42-school';
import GithubProvider from 'next-auth/providers/github';
import DiscordProvider from 'next-auth/providers/discord';
import { decodeJWT, encodeJWT } from '@lib/jwt';
import GithubProvider from 'next-auth/providers/github';

// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
Expand All @@ -17,8 +17,8 @@ export default NextAuth({
clientSecret: process.env.FORTY_TWO_CLIENT_SECRET as string,
}),
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
// https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
// @ts-ignore
scope: 'user,user:email,user:follow',
Expand Down

0 comments on commit 91e6db1

Please sign in to comment.