Skip to content

Commit 58ef0a3

Browse files
authored
Merge branch 'v3-assessment' into unauthorized-assessment-redirects
2 parents c1a1cba + 5dc10d8 commit 58ef0a3

16 files changed

Lines changed: 367 additions & 131 deletions

File tree

apps/auth/src/lib/constants.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
export const MANAGER_COOKIE_NAME = 'next-auth.session-token'
22
export const PARTICIPANT_COOKIE_NAME = 'next-auth.participant-session-token'
33

4-
export const STUDENT_REDIRECT_COOKIE_NAME =
5-
process.env.NODE_ENV === 'production'
6-
? '__Secure-klicker_student_redirect_to'
7-
: 'klicker_student_redirect_to'
8-
export const LECTURER_REDIRECT_COOKIE_NAME =
9-
process.env.NODE_ENV === 'production'
10-
? '__Secure-klicker_lecturer_redirect_to'
11-
: 'klicker_lecturer_redirect_to'
4+
export const STUDENT_REDIRECT_COOKIE_NAME = 'klicker_student_redirect_to'
5+
export const LECTURER_REDIRECT_COOKIE_NAME = 'klicker_lecturer_redirect_to'
126

137
export const DEFAULT_STUDENT_HOSTS = [
148
'assessment.klicker.uzh.ch',

apps/auth/src/middleware.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import {
77
STUDENT_REDIRECT_COOKIE_NAME,
88
} from './lib/constants'
99

10-
const _TTL_RAW = Number(process.env.REDIRECT_COOKIE_TTL_SECONDS || '3600')
11-
const REDIRECT_COOKIE_TTL_SECONDS =
12-
Number.isFinite(_TTL_RAW) && _TTL_RAW > 0 ? _TTL_RAW : 3600
10+
const REDIRECT_COOKIE_TTL_MS = 10000
1311

1412
function parseCsvHosts(value: string | undefined): string[] {
1513
if (!value) return []
@@ -145,7 +143,7 @@ export async function middleware(request: NextRequest) {
145143
// Set lecturer-specific cookie, scoped to auth host
146144
response.cookies.set(LECTURER_REDIRECT_COOKIE_NAME, redirectTo, {
147145
...commonCookieOpts,
148-
maxAge: REDIRECT_COOKIE_TTL_SECONDS,
146+
maxAge: REDIRECT_COOKIE_TTL_MS,
149147
})
150148
console.log('Root route: lecturer redirect cookie set')
151149
return response
@@ -174,7 +172,7 @@ export async function middleware(request: NextRequest) {
174172
// Set lecturer-specific cookie, scoped to auth host
175173
response.cookies.set(LECTURER_REDIRECT_COOKIE_NAME, redirectTo, {
176174
...commonCookieOpts,
177-
maxAge: REDIRECT_COOKIE_TTL_SECONDS,
175+
maxAge: REDIRECT_COOKIE_TTL_MS,
178176
})
179177
console.log(
180178
'Lecturer route: set cookie and redirect to index UI:',
@@ -204,7 +202,7 @@ export async function middleware(request: NextRequest) {
204202
// Set student-specific cookie, scoped to auth host
205203
response.cookies.set(STUDENT_REDIRECT_COOKIE_NAME, redirectTo, {
206204
...commonCookieOpts,
207-
maxAge: REDIRECT_COOKIE_TTL_SECONDS,
205+
maxAge: REDIRECT_COOKIE_TTL_MS,
208206
})
209207
console.log('Student route: cookie set, rendering student login page')
210208
return response
@@ -317,7 +315,7 @@ export async function middleware(request: NextRequest) {
317315
: LECTURER_REDIRECT_COOKIE_NAME
318316
response.cookies.set(cookieName, cb, {
319317
...commonCookieOpts,
320-
maxAge: REDIRECT_COOKIE_TTL_SECONDS,
318+
maxAge: REDIRECT_COOKIE_TTL_MS,
321319
})
322320
console.log('Set redirect cookie on signin:', {
323321
cb,

apps/auth/src/pages/api/auth/[...nextauth].ts

Lines changed: 18 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,18 @@ import { sendTeamsNotifications } from '@/lib/util'
1010
import { PrismaAdapter } from '@auth/prisma-adapter'
1111
import { prisma } from '@klicker-uzh/prisma'
1212
import { UserLoginScope, UserRole } from '@klicker-uzh/prisma/client'
13-
import { JWTPayload, signJWT, verifyJWT } from '@klicker-uzh/util'
13+
import {
14+
collectAllEmails,
15+
deriveCookieDomainFromURL,
16+
extractProviderFromAffiliationId,
17+
generateRandomString,
18+
JWTPayload,
19+
parseCookiesHeader,
20+
parseCsvHosts,
21+
reduceCatalyst,
22+
signJWT,
23+
verifyJWT,
24+
} from '@klicker-uzh/util'
1425
import bcrypt from 'bcryptjs'
1526
import crypto from 'crypto'
1627
import type { NextApiRequest, NextApiResponse } from 'next'
@@ -28,31 +39,6 @@ if (!process.env.APP_ORIGIN_AUTH) {
2839

2940
// Context detection: prefer explicit URL params and paths; fall back to
3041
// referer and an ephemeral redirect cookie set by middleware on signin.
31-
function parseCookies(req: NextApiRequest): Record<string, string> {
32-
const cookieHeader = req.headers.cookie || ''
33-
const map: Record<string, string> = {}
34-
cookieHeader.split(';').forEach((part) => {
35-
const [rawKey, ...rawVal] = part.split('=')
36-
if (!rawKey) return
37-
const key = rawKey.trim()
38-
const value = rawVal.join('=').trim()
39-
if (!key) return
40-
try {
41-
map[key] = decodeURIComponent(value)
42-
} catch {
43-
map[key] = value
44-
}
45-
})
46-
return map
47-
}
48-
49-
function parseCsvHosts(value?: string | null): string[] {
50-
if (!value) return []
51-
return value
52-
.split(',')
53-
.map((s) => s.trim())
54-
.filter(Boolean)
55-
}
5642

5743
function getStudentHosts(): string[] {
5844
const env = parseCsvHosts(process.env.AUTH_STUDENT_ALLOWED_HOSTS)
@@ -79,7 +65,7 @@ function getAuthContext(
7965
participant?: string
8066
callbackUrl?: string
8167
}
82-
const cookies = parseCookies(req)
68+
const cookies = parseCookiesHeader(req.headers.cookie)
8369
const studentRedirect = cookies[STUDENT_REDIRECT_COOKIE_NAME]
8470
const lecturerRedirect = cookies[LECTURER_REDIRECT_COOKIE_NAME]
8571

@@ -161,22 +147,6 @@ export interface ExtendedUser {
161147
catalystIndividual: boolean
162148
}
163149

164-
function reduceCatalyst(acc: boolean, affiliation: string) {
165-
try {
166-
const parts = affiliation.split('@')
167-
if (parts.length < 2) return acc || false
168-
169-
const domain = parts[1]
170-
if (domain?.includes('uzh.ch') || domain?.includes('usz.ch')) {
171-
return true
172-
}
173-
174-
return acc || false
175-
} catch (e) {
176-
return false
177-
}
178-
}
179-
180150
export async function decode({ token, secret }: JWTDecodeParams) {
181151
if (!token) return null
182152
const secretString = typeof secret === 'string' ? secret : secret.toString()
@@ -191,53 +161,9 @@ export async function encode({ token, secret }: JWTEncodeParams) {
191161
})
192162
}
193163

194-
function extractProviderFromAffiliationId(
195-
affiliationId: string
196-
): string | null {
197-
try {
198-
const parts = affiliationId.split('@')
199-
if (parts.length < 2) return null
164+
// extractProviderFromAffiliationId moved to @klicker-uzh/util
200165

201-
const domainParts = parts[1]?.split('.')
202-
if (!domainParts || domainParts.length === 0) return null
203-
204-
const provider = domainParts[0]
205-
return provider || null
206-
} catch {
207-
return null
208-
}
209-
}
210-
211-
function collectAllEmails(
212-
primaryEmail?: string,
213-
affiliationEmails?: string[]
214-
): string[] {
215-
const emails = []
216-
if (primaryEmail) emails.push(primaryEmail.toLowerCase())
217-
if (affiliationEmails) {
218-
emails.push(...affiliationEmails.map((email) => email.toLowerCase()))
219-
}
220-
return emails.filter(Boolean)
221-
}
222-
223-
function generateRandomString(length: number) {
224-
let result = ''
225-
let characters
226-
for (let i = 0; i < length; i++) {
227-
if (i === 0 || i === length - 1) {
228-
characters =
229-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
230-
} else {
231-
// TODO: re-introduce allowance for hyphens and underscores again when they are fully supported by manipulation forms
232-
characters =
233-
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
234-
// 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
235-
}
236-
const charactersLength = characters.length
237-
result += characters.charAt(Math.floor(Math.random() * charactersLength))
238-
}
239-
return result
240-
}
166+
// generateRandomString moved to @klicker-uzh/util
241167

242168
async function autoAcceptInvitations(emails: string[], participantId?: string) {
243169
let matchingParticipantId: string | undefined = participantId
@@ -289,11 +215,9 @@ async function autoAcceptInvitations(emails: string[], participantId?: string) {
289215
create: {
290216
courseId: invitation.courseId,
291217
participantId: matchingParticipantId!,
292-
isActive: true,
293-
},
294-
update: {
295-
isActive: true,
218+
isActive: false,
296219
},
220+
update: {},
297221
})
298222

299223
// Mark invitation as accepted
@@ -577,20 +501,7 @@ export default async function auth(req: NextApiRequest, res: NextApiResponse) {
577501
// label from the NEXTAUTH_URL hostname (e.g., auth.klicker.com -> klicker.com).
578502
// Avoid setting Domain for localhost or IPs.
579503
const cookieDomain: string | undefined = (() => {
580-
try {
581-
if (!process.env.NEXTAUTH_URL) return undefined
582-
const hostname = new URL(process.env.NEXTAUTH_URL).hostname
583-
if (hostname === 'localhost' || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
584-
return undefined
585-
}
586-
const parts = hostname.split('.')
587-
if (parts.length < 2) return undefined
588-
parts.shift()
589-
if (parts.length < 2) return undefined
590-
return parts.join('.')
591-
} catch {
592-
return undefined
593-
}
504+
return deriveCookieDomainFromURL(process.env.NEXTAUTH_URL)
594505
})()
595506

596507
let sharedOptions: Partial<NextAuthOptions> = {

deploy/charts/klicker-uzh-v2/templates/cm-frontend-assessment.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ metadata:
77
{{- include "chart.labels" . | nindent 4 }}
88
data:
99
# SSR-only GraphQL endpoint for Assessment PWA (internal service DNS)
10-
API_URL_SSR: "http://{{ include \"chart.fullname\" . }}-backend-assessment:3000/api/graphql"
10+
API_URL_SSR: "http://{{ include "chart.fullname" . }}-backend-assessment:3000/api/graphql"

deploy/charts/klicker-uzh-v2/templates/cm-frontend-control.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ metadata:
77
{{- include "chart.labels" . | nindent 4 }}
88
data:
99
# SSR-only GraphQL endpoint for Control frontend (internal service DNS)
10-
API_URL_SSR: "http://{{ include \"chart.fullname\" . }}-backend-graphql:3000/api/graphql"
10+
API_URL_SSR: "http://{{ include "chart.fullname" . }}-backend-graphql:3000/api/graphql"

deploy/charts/klicker-uzh-v2/templates/cm-frontend-manage.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ metadata:
77
{{- include "chart.labels" . | nindent 4 }}
88
data:
99
# SSR-only GraphQL endpoint for Manage frontend (internal service DNS)
10-
API_URL_SSR: "http://{{ include \"chart.fullname\" . }}-backend-graphql:3000/api/graphql"
10+
API_URL_SSR: "http://{{ include "chart.fullname" . }}-backend-graphql:3000/api/graphql"

deploy/charts/klicker-uzh-v2/templates/cm-frontend-pwa.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ metadata:
77
{{- include "chart.labels" . | nindent 4 }}
88
data:
99
# SSR-only GraphQL endpoint for PWA (internal service DNS)
10-
API_URL_SSR: "http://{{ include \"chart.fullname\" . }}-backend-graphql:3000/api/graphql"
10+
API_URL_SSR: "http://{{ include "chart.fullname" . }}-backend-graphql:3000/api/graphql"

deploy/env-qa-v3/_restart.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/bin/sh
22
kubectl rollout restart -n klicker-v2-qa deployment klicker-v2-qa-klicker-uzh-v2-backend-graphql
33
kubectl rollout restart -n klicker-v2-qa deployment klicker-v2-qa-klicker-uzh-v2-backend-assessment
4+
kubectl rollout restart -n klicker-v2-qa deployment klicker-v2-qa-klicker-uzh-v2-frontend-assessment
45
kubectl rollout restart -n klicker-v2-qa deployment klicker-v2-qa-klicker-uzh-v2-frontend-manage
56
kubectl rollout restart -n klicker-v2-qa deployment klicker-v2-qa-klicker-uzh-v2-frontend-pwa
67
kubectl rollout restart -n klicker-v2-qa deployment klicker-v2-qa-klicker-uzh-v2-frontend-control

packages/graphql/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ src/public/emails
66
dist/
77
instrumented/
88
hive.json
9+
10+
!invitations_dev.csv
11+
invitations_*.csv

packages/graphql/package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,17 @@
8787
"build:ts": "rollup -c",
8888
"check": "tsc --noEmit",
8989
"dev": "npm-run-all --parallel dev:graphql dev:ts",
90-
"dev:doppler": "doppler run --config dev -- pnpm run dev",
90+
"dev:doppler": "CONFIG=dev ../../util/_run_with_doppler.sh pnpm run dev",
9191
"dev:graphql": "nodemon --watch 'src/graphql/ops/*.graphql' --watch 'src/**/*.ts' --exec 'graphql-codegen --config codegen.ts'",
9292
"dev:offline": "pnpm run dev",
9393
"dev:test": "pnpm run dev",
9494
"dev:ts": "cross-env NODE_ENV=development rollup -c --watch",
9595
"generate": "graphql-codegen --config codegen.ts",
96-
"script": "ENV=development doppler run --config dev -- tsx --env-file=.env",
96+
"script": "ENV=development CONFIG=dev ../../util/_run_with_doppler.sh tsx --env-file=.env",
9797
"script:invitations:dev": "CONFIG=dev_assessment ../../util/_run_with_doppler.sh tsx src/scripts/importParticipantInvitations.ts",
98-
"script:prod": "ENV=production doppler run --config prd -- tsx",
99-
"script:stg": "ENV=development doppler run --config stg -- tsx",
98+
"script:invitations:stg": "CONFIG=stg ../../util/_run_with_doppler.sh tsx src/scripts/importParticipantInvitations.ts",
99+
"script:prod": "CONFIG=prd ../../util/_run_with_doppler.sh tsx",
100+
"script:stg": "CONFIG=stg ../../util/_run_with_doppler.sh tsx",
100101
"test": "vitest run",
101102
"test:local": "bash ./test/run-tests-local.sh",
102103
"test:watch": "vitest"

0 commit comments

Comments
 (0)