diff --git a/client/src/config/site.ts b/client/src/config/site.ts index 8144c25..857bd34 100644 --- a/client/src/config/site.ts +++ b/client/src/config/site.ts @@ -6,6 +6,6 @@ export const siteConfig = { if (PHASE_DEVELOPMENT_SERVER) { Object.assign(siteConfig, { - url: "http://megateams.durhack.com", + url: "http://megateams.durhack-dev.com", }) } diff --git a/server/src/auth/keycloak-client.ts b/server/src/auth/keycloak-client.ts index 1d67f39..91139a8 100644 --- a/server/src/auth/keycloak-client.ts +++ b/server/src/auth/keycloak-client.ts @@ -13,13 +13,14 @@ function adaptClientConfig(clientConfig: typeof keycloakConfig): ClientMetadata } satisfies ClientMetadata } -export const keycloakIssuer = await Issuer.discover(keycloakConfig.url) +const keycloakIssuerUrl = new URL(`/realms/${keycloakConfig.realm}`, keycloakConfig.baseUrl) +export const keycloakIssuer = await Issuer.discover(keycloakIssuerUrl.toString()) const keycloakClientConfig = adaptClientConfig(keycloakConfig) export const keycloakClient = new keycloakIssuer.Client(keycloakClientConfig) const keycloakAdminClient = new KeycloakAdminClient({ baseUrl: keycloakConfig.adminBaseUrl, - realmName: "durhack", + realmName: keycloakConfig.realm, }) async function fetchKeycloakClientCredentials() { diff --git a/server/src/config/default.ts b/server/src/config/default.ts index 3ea8c33..7c51b1f 100644 --- a/server/src/config/default.ts +++ b/server/src/config/default.ts @@ -3,10 +3,10 @@ import type { ConfigIn } from "./schema" export default { listen: { - host: "127.0.0.1", - port: 3101, + host: "localhost", + port: 3101, // Megateams project has ports 3100-3199 }, - hostname: "http://localhost:3101", + origin: "http://megateams.durhack-dev.com", flags: {}, csrf: { enabled: true, @@ -14,6 +14,7 @@ export default { options: { cookieOptions: { name: "durhack-megateams.x-csrf-token", + domain: "megateams.durhack-dev.com", sameSite: "strict", path: "/", secure: false, @@ -26,18 +27,20 @@ export default { session: { cookie: { name: "durhack-megateams-session", + domain: "megateams.durhack-dev.com", + sameSite: "lax", + path: "/", secure: false, }, }, megateams: { maxTeamMembers: 4, - QRCodeRedemptionURL: "https://megateams.durhack.com/hacker/redeem", }, discord: { apiEndpoint: "https://discord.com/api/v10", clientId: "yourDiscordAppClientIdHere", clientSecret: "yourDiscordAppClientSecretHere", - redirectUri: "https://megateams.durhack.com/api/discord/redirect", + redirectUri: "http://megateams.durhack-dev.com/api/discord/redirect", botToken: "yourDiscordBotTokenHere", guildID: "yourDiscordGuildIDHere", inviteLink: "https://discord.gg/xyz", @@ -46,8 +49,8 @@ export default { jsonwebtoken: { accessTokenLifetime: 1800, refreshTokenLifetime: 1209600, - issuer: "https://megateams.durhack.com", - audience: "https://megateams.durhack.com", + issuer: "http://megateams.durhack-dev.com", + audience: "http://megateams.durhack-dev.com", authorities: [ { for: TokenType.accessToken, @@ -67,11 +70,12 @@ export default { ], }, keycloak: { - url: "https://auth.durhack.com/realms/durhack", + realm: "durhack-dev", + baseUrl: "https://auth.durhack.com", adminBaseUrl: "https://admin.auth.durhack.com", clientId: "not-a-real-client-id", clientSecret: "not-a-real-client-secret", responseTypes: ["code"], - redirectUris: ["https://megateams.durhack.com/api/auth/keycloak/callback"], + redirectUris: ["http://megateams.durhack-dev.com/api/auth/keycloak/callback"], }, } satisfies ConfigIn diff --git a/server/src/config/development.ts b/server/src/config/development.ts deleted file mode 100644 index 5c6f6dc..0000000 --- a/server/src/config/development.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { DeepPartial } from "@server/types/deep-partial" -import type { ConfigIn } from "./schema" - -export default { - megateams: { - QRCodeRedemptionURL: "http://localhost:8080/hacker/redeem", - }, - keycloak: { - redirectUris: ["http://localhost:3101/api/auth/keycloak/callback"], - }, -} satisfies DeepPartial diff --git a/server/src/config/index.ts b/server/src/config/index.ts index 4762f43..367f9e2 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -30,7 +30,7 @@ export const { session: sessionConfig, discord: discordConfig, keycloak: keycloakConfig, - hostname, + origin, megateams: megateamsConfig, } = config diff --git a/server/src/config/production.ts b/server/src/config/production.ts index 7018aad..26d5431 100644 --- a/server/src/config/production.ts +++ b/server/src/config/production.ts @@ -2,15 +2,36 @@ import type { DeepPartial } from "@server/types/deep-partial" import type { ConfigIn } from "./schema" export default { + origin: "https://megateams.durhack.com", csrf: { options: { cookieOptions: { name: "__Host-durhack-megateams.x-csrf-token", + domain: undefined, + path: "/", secure: true, + sameSite: "strict", }, }, }, session: { - cookie: { secure: true }, + cookie: { + name: "__Host-durhack-megateams-session", + domain: undefined, + path: "/", + secure: true, + sameSite: "lax", + }, + }, + discord: { + redirectUri: "https://megateams.durhack.com/api/discord/redirect", + }, + jsonwebtoken: { + issuer: "https://megateams.durhack.com", + audience: "https://megateams.durhack.com", }, + keycloak: { + realm: "durhack", + redirectUris: ["https://megateams.durhack.com/api/auth/keycloak/redirect"], + } } satisfies DeepPartial diff --git a/server/src/config/schema.ts b/server/src/config/schema.ts index 23825d4..e8681c2 100644 --- a/server/src/config/schema.ts +++ b/server/src/config/schema.ts @@ -10,6 +10,7 @@ export const cookie_options_schema = z.object({ sameSite: z.union([z.literal("none"), z.literal("lax"), z.literal("strict")]).optional(), path: z.string().optional(), secure: z.boolean(), + domain: z.string().optional(), }) export const doubleCsrfOptionsSchema = z.object({ @@ -29,7 +30,8 @@ export const sessionOptionsSchema = z.object({ }) export const keycloakOptionsSchema = z.object({ - url: z.string().url(), + realm: z.string(), + baseUrl: z.string().url(), adminBaseUrl: z.string().url(), clientId: z.string(), clientSecret: z.string(), @@ -50,7 +52,7 @@ export const discordOptionsSchema = z.object({ export const configSchema = z.object({ listen: listenOptionsSchema, - hostname: z.string().url(), + origin: z.string().url(), flags: z.object({}), csrf: z.object({ enabled: z.boolean(), @@ -62,7 +64,6 @@ export const configSchema = z.object({ session: sessionOptionsSchema, megateams: z.object({ maxTeamMembers: z.number().positive(), - QRCodeRedemptionURL: z.string().url(), }), discord: discordOptionsSchema, keycloak: keycloakOptionsSchema, diff --git a/server/src/database/index.ts b/server/src/database/index.ts index d1dcc14..5840797 100644 --- a/server/src/database/index.ts +++ b/server/src/database/index.ts @@ -1,10 +1,10 @@ import { ClientError } from "@otterhttp/errors" import { type Prisma, PrismaClient } from "@prisma/client" +import { getChallengePointsByUser } from "@prisma/client/sql" import assert from "node:assert/strict" import { decodeTeamJoinCode } from "@server/common/decode-team-join-code" -import { megateamsConfig } from "@server/config" -import { getChallengePointsByUser } from "@prisma/client/sql" +import { megateamsConfig, origin } from "@server/config" export type Area = Prisma.AreaGetPayload<{ select: undefined }> export type Megateam = Prisma.MegateamGetPayload<{ select: undefined }> @@ -123,7 +123,7 @@ export const prisma = basePrisma.$extends({ needs: { payload: true }, compute(qrCode) { const redemptionUrlSearchParams = new URLSearchParams({ qr_id: qrCode.payload }) - return `${megateamsConfig.QRCodeRedemptionURL}?${redemptionUrlSearchParams.toString()}` + return `${origin}/hacker/redeem?${redemptionUrlSearchParams.toString()}` }, }, }, diff --git a/server/src/main.ts b/server/src/main.ts index 11caa41..a5d0b90 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -4,7 +4,7 @@ import { App } from "@otterhttp/app" import { Server as SocketIO } from "socket.io" import { matchSignedCookie, signCookie, unsignCookieOrThrow } from "@server/auth/cookies" -import { listenConfig } from "@server/config" +import { origin, listenConfig } from "@server/config" import { apiErrorHandler } from "@server/routes/error-handling" import { Request } from "./request" @@ -54,7 +54,7 @@ async function main() { server.listen(listenConfig.port, listenConfig.host, () => { console.log( - `> Server listening on http://${listenConfig.host}:${listenConfig.port} as ${dev ? "development" : environment}`, + `> Server listening on http://${listenConfig.host}:${listenConfig.port} as ${dev ? "development" : environment}, access via ${origin}`, ) }) } diff --git a/server/src/routes/auth/keycloak/keycloak-handlers.ts b/server/src/routes/auth/keycloak/keycloak-handlers.ts index 048d72b..a3679a0 100644 --- a/server/src/routes/auth/keycloak/keycloak-handlers.ts +++ b/server/src/routes/auth/keycloak/keycloak-handlers.ts @@ -5,7 +5,7 @@ import { type Client, generators } from "openid-client" import { adaptTokenSetToDatabase } from "@server/auth/adapt-token-set" import { keycloakClient } from "@server/auth/keycloak-client" import { getSession } from "@server/auth/session" -import { hostname } from "@server/config" +import { origin } from "@server/config" import { type User, prisma } from "@server/database" import type { Middleware } from "@server/types" @@ -41,7 +41,7 @@ export class KeycloakHandlers { } } - static redirectUri = new URL("/api/auth/keycloak/callback", hostname).toString() + static redirectUri = new URL("/api/auth/keycloak/callback", origin).toString() oauth2FlowCallback(): Middleware { return async (request: Request & { user?: User }, response: Response, next: NextFunction) => { diff --git a/server/src/routes/points/points-handlers.ts b/server/src/routes/points/points-handlers.ts index 11e24c0..40e22f4 100644 --- a/server/src/routes/points/points-handlers.ts +++ b/server/src/routes/points/points-handlers.ts @@ -65,12 +65,24 @@ class PointHandlers { }) } - const new_instance = await prisma.point.create({ - data: parsedPayload, + const adminClient = await getKeycloakAdminClient() + const userProfile = await adminClient.users.findOne({ id: parsedPayload.redeemerUserId }) + + if (userProfile == null) { + throw new ClientError(`User ID ${parsedPayload.redeemerUserId} does not correspond to a keycloak user.`, { + statusCode: HttpStatus.UnprocessableEntity, + expected: true, + }) + } + + await prisma.user.upsert({ + where: { keycloakUserId: parsedPayload.redeemerUserId }, + create: { keycloakUserId: parsedPayload.redeemerUserId, points: { create: parsedPayload } }, + update: { points: { create: parsedPayload } }, }) response.status(200) - response.json({ status: response.statusCode, message: "OK", data: new_instance }) + response.json({ status: response.statusCode, message: "OK" }) } }