Commit 7f3569af authored by Yechang's avatar Yechang
Browse files

feat: new auth

parent 86bf854d
Loading
Loading
Loading
Loading
Loading
+11 −4
Original line number Diff line number Diff line
@@ -12,15 +12,20 @@ COPY pnpm-lock.yaml ./
RUN pnpm fetch

COPY --chown=node:node . .
RUN pnpm install -r --offline
RUN pnpm prisma:generate

FROM builder as migration
ENV PRISMA_ENGINES_MIRROR=https://registry.npmmirror.com/-/binary/prisma
RUN pnpm install -r --offline && \ 
    pnpm prisma:generate && \
    pnpm prune --production

CMD pnpm prisma:deploy

FROM builder as building_stage
ENV NODE_ENV production

ENV PRISMA_ENGINES_MIRROR=https://registry.npmmirror.com/-/binary/prisma

ARG ORIGIN=https://lms.sustech.cloud
ENV ORIGIN $ORIGIN

@@ -30,8 +35,10 @@ ENV PUBLIC_COMMIT_SHORT_SHA $COMMIT_SHORT_SHA
ARG SENTRY_DSN
ENV PUBLIC_SENTRY_DSN $SENTRY_DSN

RUN NODE_OPTIONS=--max_old_space_size=4096 pnpm run build \
    && pnpm prune --production
RUN pnpm install -r --offline && \
    pnpm prisma:generate && \
    NODE_OPTIONS=--max_old_space_size=4096 pnpm run build && \
    pnpm prune --production

FROM node:20-alpine

+4 −1
Original line number Diff line number Diff line
@@ -5,7 +5,10 @@ import type { User } from '@prisma/client';
// for information about these interfaces
declare global {
	namespace App {
		// interface Error {}
		interface Error {
			code?: string;
			message?: string;
		}
		interface Locals {
			user: User | null;
			session: import('lucia').Session | null;
+10 −5
Original line number Diff line number Diff line
import { OAuth2Client } from 'oslo/oauth2';
import { env } from '$env/dynamic/private';

const clientId = env.CLIENT_ID || 'test1';
export const credentials = env.CLIENT_SECRET || 'test1';
const redirectURI = env.CLIENT_CALLBACK || 'http://sustech-spaces.test/api/auth/callback';
const clientId = env.CLIENT_ID || '691d069c-acfb-4125-87ab-d175ac686410';
export const credentials = env.CLIENT_SECRET || 'm7z877Z7VkmD3a2fj9.JUK9oCK';
const redirectURI = env.CLIENT_CALLBACK || 'http://sustech-lms.localhost/auth/callback';

const authorizeEndpoint = 'https://im.sustech.cloud/oidc/auth';
const tokenEndpoint = 'https://im.sustech.cloud/oidc/token';
// const authorizeEndpoint = 'https://127.0.0.1:4444/oauth2/auth';
// const tokenEndpoint = 'http://127.0.0.1:4444/oauth2/token';
// export const userInfoEndpoint = 'http://127.0.0.1:4444/userinfo';

const authorizeEndpoint = 'https://sustech.cloud/hydra/oauth2/auth';
const tokenEndpoint = 'https://sustech.cloud/hydra/oauth2/token';
export const userInfoEndpoint = 'https://sustech.cloud/hydra/userinfo';

const oauth2Client = new OAuth2Client(clientId, authorizeEndpoint, tokenEndpoint, {
	redirectURI
+39 −0
Original line number Diff line number Diff line
<script lang="ts">
	import { goto } from '$app/navigation';
</script>

<div class="container mx-auto flex min-h-screen items-center px-6 py-12">
	<div class="mx-auto flex max-w-sm flex-col items-center text-center">
		<p class="rounded-full bg-blue-50 p-3 text-sm font-medium text-blue-500 dark:bg-gray-800">
			<svg
				xmlns="http://www.w3.org/2000/svg"
				fill="none"
				viewBox="0 0 24 24"
				stroke-width="2"
				stroke="currentColor"
				class="h-6 w-6"
			>
				<path
					stroke-linecap="round"
					stroke-linejoin="round"
					d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
				/>
			</svg>
		</p>
		<h1 class="mt-3 text-2xl font-semibold text-gray-800 dark:text-white md:text-3xl">Error</h1>
		<!-- <p class="mt-4 text-gray-500 dark:text-gray-400">
			The page you are looking for doesn't exist. Here are some helpful links:
		</p> -->

		<div class="mt-6 flex w-full shrink-0 items-center gap-x-3 sm:w-auto">
			<button
				on:click={() => {
					goto('/');
				}}
				class="w-1/2 shrink-0 rounded-lg bg-blue-500 px-5 py-2 text-sm tracking-wide text-white transition-colors duration-200 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500 sm:w-auto"
			>
				Take me home
			</button>
		</div>
	</div>
</div>
+63 −44
Original line number Diff line number Diff line
import { error, type RequestEvent } from '@sveltejs/kit';
import { error } from '@sveltejs/kit';
import { OAuth2RequestError } from 'oslo/oauth2';
import z, { ZodError } from "zod"
import z, { ZodError } from 'zod';

import oauth2Client, { credentials } from '$lib/server/auth/sustech-cloud';
import oauth2Client, { credentials, userInfoEndpoint } from '$lib/server/auth/sustech-cloud';
import { db } from '$lib/server/db';
import _ from 'lodash';
import { lucia } from '$lib/server/auth';
import type { LuciaUser } from '@prisma/client';
import { log } from '$lib/server/log';
import type { PageServerLoad } from './$types';

const sustechUserSchema = z.object({
	sub: z.string(),
	nickname: z.string().nullish(),
	picture: z.string().nullish(),
	sustech_id: z.number(),
	sustech_email: z.string()
})
	sustech_email: z.string().or(z.array(z.string()))
});

export async function GET(event: RequestEvent): Promise<Response> {
	const code = event.url.searchParams.get('code');
export const load = (async (event) => {
	const state = event.url.searchParams.get('state');

	const storedState = event.cookies.get('_oauth_state') ?? null;
	if (!code || !state || !storedState || state !== storedState) {
		return new Response(null, {
			status: 400
	if (!state || state !== storedState) {
		error(400, {
			code: 'non_state',
			message: ''
		});
	}
	event.cookies.delete('_oauth_state', { path: '/' });

	const err = event.url.searchParams.get('error');
	const errDesc = event.url.searchParams.get('error_description');
	if (!_.isNil(err)) {
		error(400, {
			code: err || '',
			message: errDesc || ''
		});
	}

	const code = event.url.searchParams.get('code');
	const codeVerifier = event.cookies.get('_oauth_code_verifier') ?? null;
	if (!code || !codeVerifier) {
		error(400, {
			code: err || '',
			message: errDesc || ''
		});
	}
	event.cookies.delete('_oauth_code_verifier', { path: '/' });

	try {
		const tokens = await oauth2Client.validateAuthorizationCode(code, {
			credentials,
			authenticateWith: 'request_body'
			authenticateWith: 'request_body',
			codeVerifier
		});

		const userResponse = await fetch('https://im.sustech.cloud/oidc/me', {
		const userResponse = await fetch(userInfoEndpoint, {
			headers: {
				Authorization: `Bearer ${tokens.access_token}`
			}
		});
		const userData = sustechUserSchema.parse(await userResponse.json());
		log.info(userData);

		const data = await userResponse.json();
		log.info({ data });
		const userData = sustechUserSchema.parse(data);

		const guaranteeLuciaUser = async () => {
			const luciaUser = await db.luciaUser.findUnique({
@@ -50,7 +71,7 @@ export async function GET(event: RequestEvent): Promise<Response> {
				}
			});
			if (!_.isNil(luciaUser)) {
				return luciaUser
				return luciaUser;
			}

			const user = await db.user.findFirst({
@@ -76,40 +97,38 @@ export async function GET(event: RequestEvent): Promise<Response> {
					id: userData.sub,
					user: {
						create: {
							email: userData.sustech_email,
							email: '',
							sustechId: userData.sustech_id,
							nickname: userData.nickname || userData.sustech_id.toString(),
							image: userData.picture || ""
							image: userData.picture || ''
						}
					}
				}
			});



			// check any pending class invitation
			const pendingClasses = await db.class.findMany({
				where: {
					pendingStudentList: {
						array_contains: userData.sustech_id,
					},
				},
			})
						array_contains: userData.sustech_id
					}
				}
			});

			await db.classesOnUsers.createMany({
				data: pendingClasses.map(c => {
				data: pendingClasses.map((c) => {
					return {
						classId: c.id,
						role: 'STUDENT',
						userId: newLuciaUser.userId,
						status: 'APPROVED'
					}
				})
					};
				})
			});

			return newLuciaUser
		}
		const luciaUser: LuciaUser = await guaranteeLuciaUser()
			return newLuciaUser;
		};
		const luciaUser: LuciaUser = await guaranteeLuciaUser();

		const session = await lucia.createSession(luciaUser.id, {});
		const sessionCookie = lucia.createSessionCookie(session.id);
@@ -117,28 +136,28 @@ export async function GET(event: RequestEvent): Promise<Response> {
			path: '.',
			...sessionCookie.attributes
		});

		return new Response(null, {
			status: 302,
			headers: {
				Location: '/'
			}
		});
		return {};
	} catch (e) {
		log.error(e);
		// the specific error message depends on the provider
		if (e instanceof ZodError) {
			error(400, { message: 'We could not infer your sustech ID from your email. Please contact the admin for help.' })
			error(400, {
				message:
					'We could not infer your sustech ID from your email. Please contact the admin for help.'
			});
		}
		if (e instanceof OAuth2RequestError) {
			// invalid code
			log.error(e.request.url);
			return new Response(null, {
				status: 400
			error(400, {
				code: err || '',
				message: errDesc || ''
			});
		}
		return new Response(null, {
			status: 500

		error(500, {
			code: err || '',
			message: errDesc || ''
		});
	}
}
}) satisfies PageServerLoad;
Loading