|
|
@@ -1,3 +1,5 @@
|
|
|
+import { redirect } from '@sveltejs/kit';
|
|
|
+import { ObjectId } from 'mongodb';
|
|
|
import { dev } from '$app/environment';
|
|
|
import { env } from '$env/dynamic/private';
|
|
|
import jwt from 'jsonwebtoken';
|
|
|
@@ -5,6 +7,17 @@ import jwt from 'jsonwebtoken';
|
|
|
export const SESSION_COOKIE = 'torus_session';
|
|
|
const TOKEN_TTL = '7d';
|
|
|
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|
|
+const USERS = 'users';
|
|
|
+
|
|
|
+/**
|
|
|
+ * Authenticated user returned to private routes.
|
|
|
+ * Password is never included.
|
|
|
+ * @typedef {object} AuthUser
|
|
|
+ * @property {string} id
|
|
|
+ * @property {string} uuid
|
|
|
+ * @property {string} name
|
|
|
+ * @property {string} email
|
|
|
+ */
|
|
|
|
|
|
/**
|
|
|
* @returns {string}
|
|
|
@@ -73,3 +86,67 @@ export function setSessionCookie(cookies, token) {
|
|
|
export function clearSessionCookie(cookies) {
|
|
|
cookies.delete(SESSION_COOKIE, { path: '/' });
|
|
|
}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Read the session cookie, verify the JWT, then load the user by both
|
|
|
+ * MongoDB id and uuid. uuid must still match the DB value so rotating it
|
|
|
+ * invalidates sessions on all devices.
|
|
|
+ *
|
|
|
+ * @param {import('mongodb').Db} db
|
|
|
+ * @param {import('@sveltejs/kit').Cookies} cookies
|
|
|
+ * @returns {Promise<AuthUser | null>}
|
|
|
+ */
|
|
|
+export async function getUser(db, cookies) {
|
|
|
+ const token = cookies.get(SESSION_COOKIE);
|
|
|
+ if (!token) return null;
|
|
|
+
|
|
|
+ const session = verifySessionToken(token);
|
|
|
+ if (!session) {
|
|
|
+ clearSessionCookie(cookies);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ObjectId.isValid(session.id)) {
|
|
|
+ clearSessionCookie(cookies);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const doc = await db.collection(USERS).findOne(
|
|
|
+ {
|
|
|
+ _id: new ObjectId(session.id),
|
|
|
+ uuid: session.uuid
|
|
|
+ },
|
|
|
+ {
|
|
|
+ projection: {
|
|
|
+ password: 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!doc) {
|
|
|
+ clearSessionCookie(cookies);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: String(doc._id),
|
|
|
+ uuid: String(doc.uuid),
|
|
|
+ name: String(doc.name ?? ''),
|
|
|
+ email: String(doc.email ?? '')
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Require an authenticated user for a private route.
|
|
|
+ * Redirects to /login when the session is missing or invalid.
|
|
|
+ *
|
|
|
+ * @param {{ locals: { db: import('mongodb').Db }, cookies: import('@sveltejs/kit').Cookies }} event
|
|
|
+ * @returns {Promise<AuthUser>}
|
|
|
+ */
|
|
|
+export async function requireUser(event) {
|
|
|
+ const user = await getUser(event.locals.db, event.cookies);
|
|
|
+ if (!user) {
|
|
|
+ redirect(303, '/login');
|
|
|
+ }
|
|
|
+ return user;
|
|
|
+}
|