diff --git a/compose-dev.yml b/compose-dev.yml index 9a1b918..180cf13 100644 --- a/compose-dev.yml +++ b/compose-dev.yml @@ -11,3 +11,5 @@ services: # Only necessary for externaly hosted databases such as NeonDB volumes: - ./sql:/sql + +# docker compose -f ./compose-dev.yml up --build \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index 65be1f9..7f4085a 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -3,6 +3,7 @@ declare global { interface Locals { user: import("lucia").User | null; session: import("lucia").Session | null; + isServerSetup: boolean; } } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 09bd7ea..caa3471 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,6 +1,9 @@ import { lucia } from "$lib/server/auth"; -import type { Handle } from "@sveltejs/kit"; +import { redirect, type Handle } from "@sveltejs/kit"; import { sequence } from '@sveltejs/kit/hooks'; +import { db } from "$lib/db/db.server"; +import { userTable } from "$lib/db/schema"; +import { eq } from "drizzle-orm"; export const authHook: Handle = async ({ event, resolve }) => { const sessionId = event.cookies.get(lucia.sessionCookieName); @@ -53,4 +56,27 @@ export const themeHook: Handle = async ({ event, resolve }) => { return await resolve(event); } -export const handle = sequence(authHook, themeHook); +export const setupAdminUser: Handle = async ({ event, resolve }) => { + // Check if an admin user exists + /* let result = await db + .select() + .from(userVisitedAdventures) + .where(eq(userVisitedAdventures.userId, event.locals.user.id)) + .execute();*/ + let adminUser = await db + .select() + .from(userTable) + .where(eq(userTable.role, 'admin')) + .execute(); + // If an admin user exists, return the resolved event + if (adminUser != null && adminUser.length > 0) { + event.locals.isServerSetup = true; + return await resolve(event); + } + + console.log("No admin user found"); + event.locals.isServerSetup = false; + return await resolve(event); +}; + +export const handle = sequence(setupAdminUser, authHook, themeHook); diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index d843f2a..adc26d8 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,12 +1,15 @@ +import { goto } from "$app/navigation"; import type { LayoutServerLoad, PageServerLoad } from "./$types"; export const load: LayoutServerLoad = async (event) => { if (event.locals.user) { return { user: event.locals.user, + isServerSetup: event.locals.isServerSetup, }; } return { user: null, + isServerSetup: event.locals.isServerSetup, }; }; \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 22582f2..9e4a2cf 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,6 +4,21 @@ import Navbar from "$lib/components/Navbar.svelte"; import type { SubmitFunction } from "@sveltejs/kit"; import "../app.css"; + import { goto } from "$app/navigation"; + import { onMount } from "svelte"; + import { page } from "$app/stores"; + + let isServerSetup = data.isServerSetup; + + onMount(() => { + console.log("isServerSetup", isServerSetup); + if (!isServerSetup && $page.url.pathname !== "/setup") { + goto("/setup"); + } + if (isServerSetup && $page.url.pathname == "/setup") { + goto("/"); + } + }); diff --git a/src/routes/settings/+page.server.ts b/src/routes/settings/+page.server.ts index 25695c7..c2cbb5b 100644 --- a/src/routes/settings/+page.server.ts +++ b/src/routes/settings/+page.server.ts @@ -4,6 +4,7 @@ import { db } from "$lib/db/db.server"; import { userTable } from "$lib/db/schema"; import { eq } from "drizzle-orm"; import { Argon2id } from "oslo/password"; +import type { DatabaseUser } from "$lib/server/auth"; export const load: PageServerLoad = async (event) => { if (event.locals.user) @@ -42,6 +43,22 @@ export const actions: Actions = { }; } + const usernameTaken = await db + .select() + .from(userTable) + .where(eq(userTable.username, username)) + .limit(1) + .then((results) => results[0] as unknown as DatabaseUser | undefined); + + if (usernameTaken) { + return { + status: 400, + body: { + message: "Username taken!" + } + }; + } + if (password) { let hashedPassword = await new Argon2id().hash(password); console.log(hashedPassword) diff --git a/src/routes/setup/+page.server.ts b/src/routes/setup/+page.server.ts new file mode 100644 index 0000000..7fd366a --- /dev/null +++ b/src/routes/setup/+page.server.ts @@ -0,0 +1,121 @@ +// routes/signup/+page.server.ts +import { lucia } from "$lib/server/auth"; +import { fail, redirect } from "@sveltejs/kit"; +import { generateId } from "lucia"; +import { Argon2id } from "oslo/password"; +import { db } from "$lib/db/db.server"; +import type { DatabaseUser } from "$lib/server/auth"; + +import type { Actions } from "./$types"; +import { userTable } from "$lib/db/schema"; +import { eq } from "drizzle-orm"; + +export const actions: Actions = { + default: async (event) => { + const formData = await event.request.formData(); + const username = formData.get("username"); + const password = formData.get("password"); + const firstName = formData.get("first_name"); + const lastName = formData.get("last_name"); + // username must be between 4 ~ 31 characters, and only consists of lowercase letters, 0-9, -, and _ + // keep in mind some database (e.g. mysql) are case insensitive + + // check all to make sure all fields are provided + if (!username || !password || !firstName || !lastName) { + return fail(400, { + message: "All fields are required", + }); + } + + if ( + typeof username !== "string" || + username.length < 3 || + username.length > 31 || + !/^[a-z0-9_-]+$/.test(username) + ) { + return fail(400, { + message: "Invalid username", + }); + } + if ( + typeof password !== "string" || + password.length < 6 || + password.length > 255 + ) { + return fail(400, { + message: "Invalid password", + }); + } + + if ( + typeof firstName !== "string" || + firstName.length < 1 || + firstName.length > 255 + ) { + return fail(400, { + message: "Invalid first name", + }); + } + + if ( + typeof lastName !== "string" || + lastName.length < 1 || + lastName.length > 255 + ) { + return fail(400, { + message: "Invalid last name", + }); + } + + const userId = generateId(15); + const hashedPassword = await new Argon2id().hash(password); + + const usernameTaken = await db + .select() + .from(userTable) + .where(eq(userTable.username, username)) + .limit(1) + .then((results) => results[0] as unknown as DatabaseUser | undefined); + + if (usernameTaken) { + return fail(400, { + message: "Username already taken", + }); + } + + let adminUser = await db + .select() + .from(userTable) + .where(eq(userTable.role, 'admin')) + .execute(); + + if (adminUser != null && adminUser.length > 0) { + return fail(400, { + message: "Admin user already exists", + }); + } + + await db + .insert(userTable) + .values({ + id: userId, + username: username, + first_name: firstName, + last_name: lastName, + hashed_password: hashedPassword, + signup_date: new Date(), + role: "admin", + last_login: new Date(), + } as DatabaseUser) + .execute(); + + const session = await lucia.createSession(userId, {}); + const sessionCookie = lucia.createSessionCookie(session.id); + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: ".", + ...sessionCookie.attributes, + }); + + redirect(302, "/"); + }, +}; diff --git a/src/routes/setup/+page.svelte b/src/routes/setup/+page.svelte new file mode 100644 index 0000000..2c7a155 --- /dev/null +++ b/src/routes/setup/+page.svelte @@ -0,0 +1,44 @@ + + +

AdventureLog Setup

+ + +

+ Welcome to AdventureLog! Please follow the steps below to setup your server. +

+ + +

Create Admin User

+ +
+
+ +
+ +
+ +
+ +
+ +
+
diff --git a/startup.sh b/startup.sh index ef4e7b9..d6991f1 100644 --- a/startup.sh +++ b/startup.sh @@ -24,6 +24,7 @@ run_sql_scripts() { echo "Finished running SQL scripts." } + # Start your application here # Print message echo "Starting AdventureLog" @@ -33,6 +34,9 @@ if [ -z "$SKIP_DB_WAIT" ] || [ "$SKIP_DB_WAIT" = "false" ]; then wait_for_db fi +# Wait for the database to start up +setup_admin_user + # generate the schema # npm run generate