1
0
Fork 0
mirror of https://github.com/seanmorley15/AdventureLog.git synced 2025-07-28 17:29:36 +02:00

Added auth!

This commit is contained in:
Sean Morley 2024-04-03 00:51:12 +00:00
parent 3fd6d021f7
commit 372db59211
18 changed files with 1887 additions and 20 deletions

View file

@ -0,0 +1,15 @@
CREATE TABLE IF NOT EXISTS "session" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user" (
"id" text PRIMARY KEY NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,2 @@
ALTER TABLE "user" ADD COLUMN "username" text NOT NULL;--> statement-breakpoint
ALTER TABLE "user" ADD COLUMN "hashed_password" text NOT NULL;

View file

@ -0,0 +1 @@
ALTER TABLE "user" ALTER COLUMN "hashed_password" SET DATA TYPE varchar;

View file

@ -0,0 +1,123 @@
{
"id": "ccf7c336-c61f-452f-822b-b3b039bb20f9",
"prevId": "45d98527-f0a9-44fc-9658-d3c461afed95",
"version": "5",
"dialect": "pg",
"tables": {
"featuredAdventures": {
"name": "featuredAdventures",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sharedAdventures": {
"name": "sharedAdventures",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"data": {
"name": "data",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,135 @@
{
"id": "e91dda33-e04e-4e99-a297-21a34aa35493",
"prevId": "ccf7c336-c61f-452f-822b-b3b039bb20f9",
"version": "5",
"dialect": "pg",
"tables": {
"featuredAdventures": {
"name": "featuredAdventures",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sharedAdventures": {
"name": "sharedAdventures",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"data": {
"name": "data",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"hashed_password": {
"name": "hashed_password",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -0,0 +1,135 @@
{
"id": "b0849b3e-02e1-42e1-b07c-6fa613c98e82",
"prevId": "e91dda33-e04e-4e99-a297-21a34aa35493",
"version": "5",
"dialect": "pg",
"tables": {
"featuredAdventures": {
"name": "featuredAdventures",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"location": {
"name": "location",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"session_user_id_user_id_fk": {
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"sharedAdventures": {
"name": "sharedAdventures",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"data": {
"name": "data",
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"hashed_password": {
"name": "hashed_password",
"type": "varchar",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -29,6 +29,27 @@
"when": 1712083977580,
"tag": "0003_clammy_goblin_queen",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1712103855532,
"tag": "0004_smart_maelstrom",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1712104331399,
"tag": "0005_glamorous_pixie",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1712105206127,
"tag": "0006_melted_leech",
"breakpoints": true
}
]
}

1193
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,11 +21,13 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.12",
"@types/pg": "^8.11.4",
"autoprefixer": "^10.4.19",
"daisyui": "^4.9.0",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.20.14",
"pg": "^8.11.4",
"lucia": "^3.1.1",
"pg": "^8.11.5",
"postcss": "^8.4.38",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
@ -36,7 +38,9 @@
},
"type": "module",
"dependencies": {
"@lucia-auth/adapter-drizzle": "^1.0.7",
"drizzle-orm": "^0.30.6",
"oslo": "^1.2.0",
"postgres": "^3.4.4"
}
}

11
src/app.d.ts vendored
View file

@ -1,12 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
interface Locals {
user: import("lucia").User | null;
session: import("lucia").Session | null;
}
}
}

32
src/hooks.server.ts Normal file
View file

@ -0,0 +1,32 @@
import { lucia } from "$lib/server/auth";
import type { Handle } from "@sveltejs/kit";
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(lucia.sessionCookieName);
if (!sessionId) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
// sveltekit types deviates from the de-facto standard
// you can use 'as any' too
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes,
});
}
if (!session) {
const sessionCookie = lucia.createBlankSessionCookie();
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes,
});
}
event.locals.user = user;
event.locals.session = session;
return resolve(event);
};

View file

@ -1,8 +1,9 @@
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import dotenv from "dotenv";
import * as schema from "$lib/db/schema";
dotenv.config();
const { DATABASE_URL } = process.env;
const client = postgres(DATABASE_URL);
export const db = drizzle(client, {});
const client = postgres(DATABASE_URL || ""); // Pass DATABASE_URL as a string argument
export const db = drizzle(client, { schema });

View file

@ -1,4 +1,11 @@
import { pgTable, json, text, serial } from "drizzle-orm/pg-core";
import {
pgTable,
text,
timestamp,
json,
serial,
varchar,
} from "drizzle-orm/pg-core";
export const featuredAdventures = pgTable("featuredAdventures", {
id: serial("id").primaryKey(),
@ -10,3 +17,22 @@ export const sharedAdventures = pgTable("sharedAdventures", {
id: text("id").primaryKey(),
data: json("data").notNull(),
});
export const userTable = pgTable("user", {
id: text("id").primaryKey(),
username: text("username").notNull(),
hashed_password: varchar("hashed_password").notNull(),
});
// export type SelectUser = typeof userTable.$inferSelect;
export const sessionTable = pgTable("session", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => userTable.id),
expiresAt: timestamp("expires_at", {
withTimezone: true,
mode: "date",
}).notNull(),
});

38
src/lib/server/auth.ts Normal file
View file

@ -0,0 +1,38 @@
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { Lucia } from "lucia";
import { dev } from "$app/environment";
import { userTable, sessionTable } from "$lib/db/schema";
import { db } from "$lib/db/db.server";
import { Argon2id } from "oslo/password";
const adapter = new DrizzlePostgreSQLAdapter(db, sessionTable, userTable);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: !dev,
},
},
getUserAttributes: (attributes) => {
return {
// attributes has the type of DatabaseUserAttributes
username: attributes.username,
};
},
});
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
}
interface DatabaseUserAttributes {
username: string;
}
export interface DatabaseUser {
id: string;
username: string;
hashed_password: string;
}

View file

@ -0,0 +1,76 @@
import { lucia } from "$lib/server/auth";
import { fail, redirect } from "@sveltejs/kit";
import { Argon2id } from "oslo/password";
import { db } from "$lib/db/db.server";
import type { Actions, PageServerLoad } from "./$types";
import type { DatabaseUser } from "$lib/server/auth";
import { userTable } from "$lib/db/schema";
import { eq } from "drizzle-orm";
export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
return redirect(302, "/");
}
return {};
};
export const actions: Actions = {
default: async (event) => {
const formData = await event.request.formData();
const username = formData.get("username");
const password = formData.get("password");
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",
});
}
const existingUser = await db
.select()
.from(userTable)
.where(eq(userTable.username, username))
.limit(1)
.then((results) => results[0] as unknown as DatabaseUser | undefined);
if (!existingUser) {
return fail(400, {
message: "Incorrect username or password",
});
}
const validPassword = await new Argon2id().verify(
existingUser.hashed_password,
password
);
if (!validPassword) {
return fail(400, {
message: "Incorrect username or password",
});
}
const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes,
});
return redirect(302, "/");
},
};

View file

@ -0,0 +1,13 @@
<!-- routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from "$app/forms";
</script>
<h1>Sign in</h1>
<form method="post" use:enhance>
<label for="username">Username</label>
<input name="username" id="username" /><br />
<label for="password">Password</label>
<input type="password" name="password" id="password" /><br />
<button>Continue</button>
</form>

View file

@ -0,0 +1,60 @@
// 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 { Actions } from "./$types";
import { userTable } from "$lib/db/schema";
export const actions: Actions = {
default: async (event) => {
const formData = await event.request.formData();
const username = formData.get("username");
const password = formData.get("password");
// 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
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",
});
}
const userId = generateId(15);
const hashedPassword = await new Argon2id().hash(password);
// TODO: check if username is already used
await db
.insert(userTable)
.values({
id: userId,
username: username,
hashed_password: hashedPassword,
})
.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, "/");
},
};

View file

@ -0,0 +1,13 @@
<!-- routes/signup/+page.svelte -->
<script lang="ts">
import { enhance } from "$app/forms";
</script>
<h1>Sign up</h1>
<form method="post" use:enhance>
<label for="username">Username</label>
<input name="username" id="username" /><br />
<label for="password">Password</label>
<input type="password" name="password" id="password" /><br />
<button>Continue</button>
</form>