Skip to content

Commit

Permalink
Merge pull request #13 from thatgurkangurk/first-user-admin
Browse files Browse the repository at this point in the history
set first user to admin + auth refactor
  • Loading branch information
thatgurkangurk authored Apr 30, 2024
2 parents cd3e548 + 440b9f4 commit 009aa14
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 78 deletions.
35 changes: 20 additions & 15 deletions drizzle/0000_moaning_silver_centurion.sql
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
CREATE TABLE `session` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action
);
CREATE TABLE IF NOT EXISTS `session`
(`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action);

--> statement-breakpoint
CREATE TABLE `user` (
`id` text PRIMARY KEY NOT NULL,
`username` text NOT NULL,
`hashed_password` text NOT NULL,
`role` text DEFAULT 'user' NOT NULL
);

CREATE TABLE IF NOT EXISTS `user` (`id` text PRIMARY KEY NOT NULL,
`username` text NOT NULL,
`hashed_password` text NOT NULL,
`role` text DEFAULT 'user' NOT NULL);

--> statement-breakpoint
CREATE UNIQUE INDEX `session_id_unique` ON `session` (`id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_id_unique` ON `user` (`id`);--> statement-breakpoint
CREATE UNIQUE INDEX `user_username_unique` ON `user` (`username`);

CREATE UNIQUE INDEX IF NOT EXISTS `session_id_unique` ON `session` (`id`);--> statement-breakpoint


CREATE UNIQUE INDEX IF NOT EXISTS `user_id_unique` ON `user` (`id`);--> statement-breakpoint


CREATE UNIQUE INDEX IF NOT EXISTS `user_username_unique` ON `user` (`username`);
24 changes: 12 additions & 12 deletions drizzle/0001_strange_carnage.sql
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
CREATE TABLE `link_click` (
`id` text PRIMARY KEY NOT NULL,
`link_id` text NOT NULL,
`timestamp` integer DEFAULT (unixepoch()) NOT NULL
);
CREATE TABLE IF NOT EXISTS `link_click` (`id` text PRIMARY KEY NOT NULL,
`link_id` text NOT NULL,
`timestamp` integer DEFAULT (unixepoch()) NOT NULL);

--> statement-breakpoint
CREATE TABLE `link` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`slug` text NOT NULL,
`target` text NOT NULL
);

CREATE TABLE IF NOT EXISTS `link` (`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`slug` text NOT NULL,
`target` text NOT NULL);

--> statement-breakpoint
CREATE UNIQUE INDEX `link_slug_unique` ON `link` (`slug`);

CREATE UNIQUE INDEX IF NOT EXISTS `link_slug_unique` ON `link` (`slug`);
10 changes: 4 additions & 6 deletions src/lib/db/schema/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,32 @@ import { relations, sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { users } from "./user";

const links = sqliteTable("link", {
export const links = sqliteTable("link", {
id: text("id").notNull().primaryKey(),
userId: text("user_id").notNull(),
slug: text("slug").notNull().unique(),
target: text("target").notNull(),
});

const linkClicks = sqliteTable("link_click", {
export const linkClicks = sqliteTable("link_click", {
id: text("id").notNull().primaryKey(),
linkId: text("link_id").notNull(),
timestamp: integer("timestamp", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),
});

const linksRelations = relations(links, ({ one, many }) => ({
export const linksRelations = relations(links, ({ one, many }) => ({
owner: one(users, {
fields: [links.userId],
references: [users.id],
}),
clicks: many(linkClicks),
}));

const linkClickRelations = relations(linkClicks, ({ one }) => ({
export const linkClickRelations = relations(linkClicks, ({ one }) => ({
link: one(links, {
fields: [linkClicks.linkId],
references: [links.id],
}),
}));

export { links, linkClicks, linksRelations, linkClickRelations };
4 changes: 1 addition & 3 deletions src/lib/db/schema/serverSettings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { sqliteTable, integer } from "drizzle-orm/sqlite-core";

const serverSettings = sqliteTable("server_settings", {
export const serverSettings = sqliteTable("server_settings", {
id: integer("id").primaryKey({ autoIncrement: true }),
registrationEnabled: integer("registration_enabled", {
mode: "boolean",
Expand All @@ -13,5 +13,3 @@ const serverSettings = sqliteTable("server_settings", {
.default(true)
.notNull(),
});

export { serverSettings };
2 changes: 1 addition & 1 deletion src/lib/db/schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core";
import { nanoid } from "nanoid";
import { links } from "./link";

const userRole = text("role", { enum: ["admin", "user"] })
export const userRole = text("role", { enum: ["admin", "user"] })
.notNull()
.default("user");

Expand Down
14 changes: 14 additions & 0 deletions src/lib/server/password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { hash, verify } from "@node-rs/argon2";

async function hashPassword(password: string) {
return await hash(password);
}

async function verifyPassword(hash: string, password: string) {
return await verify(hash, password);
}

export {
hashPassword,
verifyPassword
}
42 changes: 42 additions & 0 deletions src/lib/server/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { User, Cookie, Session } from "lucia";
import { lucia } from "./auth";
import type { RequestEvent } from "@sveltejs/kit";
import { db } from "$lib/db";
import { sessions } from "$lib/db/schema/session";
import { eq } from "drizzle-orm";

async function _createSession(userOrUserId: User | string): Promise<Session> {
if (typeof userOrUserId === "string") {
const session = await lucia.createSession(userOrUserId, {});
return session;
} else {
const session = await lucia.createSession(userOrUserId.id, {});
return session;
}
}

async function createSessionCookie(user: User): Promise<Cookie>;
async function createSessionCookie(userId: string): Promise<Cookie>;
async function createSessionCookie(
userOrUserId: User | string
): Promise<Cookie> {
const session = await _createSession(userOrUserId);
const sessionCookie = lucia.createSessionCookie(session.id);
return sessionCookie;
}

async function invalidateSession(session: Session, event: RequestEvent) {
try {
await lucia.invalidateSession(session.id);
const sessionCookie = lucia.createBlankSessionCookie();
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes,
});
await db.delete(sessions).where(eq(sessions.id, session.id));
} catch (e) {
console.log(e);
}
}

export { createSessionCookie, invalidateSession };
81 changes: 81 additions & 0 deletions src/lib/server/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { db } from "$lib/db";
import { users } from "$lib/db/schema/user";
import { generateId, type User } from "lucia";
import { hashPassword } from "./password";
import { sql } from "drizzle-orm";

async function userExists(username: string) {
const existingUser = await db.query.users.findFirst({
where: (user, { eq }) => eq(user.username, username),
});

return existingUser;
}

async function getTotalUserCount() {
const [count] = await db.select({ count: sql<number>`count(*)` }).from(users);

return count.count;
}

async function isUsersEmpty() {
const count = await getTotalUserCount();
const isEmpty = count === 0;

return isEmpty;
}

async function createUser({
username,
password,
}: {
username: string;
password: string;
}): Promise<
| {
success: true;
user: User;
}
| {
success: false;
cause: "USER_EXISTS" | "UNEXPECTED_ERROR";
}
> {
if (await userExists(username))
return {
success: false,
cause: "USER_EXISTS",
};

const id = generateId(15);
const hashedPassword = await hashPassword(password);
const isFirstUser = await isUsersEmpty();

try {
const insertedUser = await db
.insert(users)
.values({
id: id,
username: username,
hashed_password: hashedPassword,
role: isFirstUser ? "admin" : "user",
})
.returning({
id: users.id,
username: users.username,
role: users.role,
});

return {
success: true,
user: insertedUser[0],
};
} catch (e) {
return {
success: false,
cause: "UNEXPECTED_ERROR",
};
}
}

export { userExists, createUser };
19 changes: 9 additions & 10 deletions src/routes/auth/login/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { lucia } from "$lib/server/auth";
import { fail, redirect } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { db } from "$lib/db";
import { loginFormSchema } from "$lib/user";
import type { PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms";
import { zod } from "sveltekit-superforms/adapters";
import { hashSync, verify } from "@node-rs/argon2";
import { hashPassword, verifyPassword } from "$lib/server/password";
import { userExists } from "$lib/server/user";
import { createSessionCookie } from "$lib/server/session";

export const load: PageServerLoad = async (event) => {
if (event.locals.user) {
Expand All @@ -28,9 +29,8 @@ export const actions: Actions = {

const { username, password } = form.data;

const existingUser = await db.query.users.findFirst({
where: (user, { eq }) => eq(user.username, username),
});
const existingUser = await userExists(username);

if (!existingUser) {
// NOTE:
// Returning immediately allows malicious actors to figure out valid usernames from response times,
Expand All @@ -41,23 +41,22 @@ export const actions: Actions = {
// Since protecting against this is non-trivial,
// it is crucial your implementation is protected against brute-force attacks with login throttling etc.
// If usernames are public, you may outright tell the user that the username is invalid.
hashSync(password);
await hashPassword(password);
return fail(400, {
message: "Incorrect username or password",
form,
});
}

const validPassword = await verify(existingUser.hashed_password, password);
const validPassword = await verifyPassword(existingUser.hashed_password, password);
if (!validPassword) {
return fail(400, {
message: "Incorrect username or password",
form,
});
});
}

const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
const sessionCookie = await createSessionCookie(existingUser);
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes,
Expand Down
9 changes: 2 additions & 7 deletions src/routes/auth/logout/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import { lucia } from "$lib/server/auth";
import { redirect } from "@sveltejs/kit";
import { fail } from "sveltekit-superforms";
import type { Actions } from "./$types";
import { invalidateSession } from "$lib/server/session";

export const actions: Actions = {
default: async (event) => {
if (!event.locals.session) {
return fail(401);
}

await lucia.invalidateSession(event.locals.session.id);
const sessionCookie = lucia.createBlankSessionCookie();
event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".",
...sessionCookie.attributes,
});
await invalidateSession(event.locals.session, event);

redirect(302, "/auth/login");
},
Expand Down
Loading

0 comments on commit 009aa14

Please sign in to comment.