diff --git a/src/app.d.ts b/src/app.d.ts index e58412e..e827dee 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -4,7 +4,7 @@ declare global { namespace App { // interface Error {} interface Locals { - user: User + user: Omit } // interface PageData {} // interface PageState {} @@ -21,7 +21,7 @@ declare global { interface User { id: string username: string - password?: string + password: string } type serviceType = 'jellyfin' | 'youtube-music' @@ -39,6 +39,7 @@ declare global { accessToken: string refreshToken?: string expiry?: number + connectionInfo?: ConnectionInfo } interface ConnectionInfo { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 86ec659..450b6bf 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -17,7 +17,7 @@ export const handle: Handle = async ({ event, resolve }) => { if (!authToken) throw redirect(303, `/login?redirect=${urlpath}`) try { - const tokenData = jwt.verify(authToken, SECRET_JWT_KEY) as User + const tokenData = jwt.verify(authToken, SECRET_JWT_KEY) as Omit event.locals.user = tokenData } catch { throw redirect(303, `/login?redirect=${urlpath}`) diff --git a/src/lib/server/users.db b/src/lib/server/users.db index 7696ed7..ab11870 100644 Binary files a/src/lib/server/users.db and b/src/lib/server/users.db differ diff --git a/src/lib/server/users.ts b/src/lib/server/users.ts index 58abf85..bdf4f85 100644 --- a/src/lib/server/users.ts +++ b/src/lib/server/users.ts @@ -1,10 +1,14 @@ -import Database from 'better-sqlite3' +import Database, { SqliteError } from 'better-sqlite3' import { generateUUID } from '$lib/utils' import { isValidURL } from '$lib/utils' const db = new Database('./src/lib/server/users.db', { verbose: console.info }) db.pragma('foreign_keys = ON') -const initUsersTable = 'CREATE TABLE IF NOT EXISTS Users(id VARCHAR(36) PRIMARY KEY, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(72) NOT NULL)' +const initUsersTable = `CREATE TABLE IF NOT EXISTS Users( + id VARCHAR(36) PRIMARY KEY, + username VARCHAR(30) UNIQUE NOT NULL, + password VARCHAR(72) NOT NULL +)` const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections( id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, @@ -16,10 +20,6 @@ const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections( )` db.exec(initUsersTable), db.exec(initConnectionsTable) -type UserQueryParams = { - includePassword?: boolean -} - interface ConnectionsTableSchema { id: string userId: string @@ -30,25 +30,19 @@ interface ConnectionsTableSchema { } export class Users { - static getUser = (id: string, params: UserQueryParams | null = null): User | undefined => { - const user = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User | undefined - if (user && !params?.includePassword) delete user.password + static getUser = (id: string): User | null => { + const user = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User | null return user } - static getUsername = (username: string, params: UserQueryParams | null = null): User | undefined => { - const user = db.prepare('SELECT * FROM Users WHERE lower(username) = ?').get(username.toLowerCase()) as User | undefined - if (user && !params?.includePassword) delete user.password + static getUsername = (username: string): User | null => { + const user = db.prepare('SELECT * FROM Users WHERE lower(username) = ?').get(username.toLowerCase()) as User | null return user } - static allUsers = (includePassword: boolean = false): User[] => { - const users = db.prepare('SELECT * FROM Users').all() as User[] - if (!includePassword) users.forEach((user) => delete user.password) - return users - } + static addUser = (username: string, hashedPassword: string): User | null => { + if (this.getUsername(username)) return null - static addUser = (username: string, hashedPassword: string): User => { const userId = generateUUID() db.prepare('INSERT INTO Users(id, username, password) VALUES(?, ?, ?)').run(userId, username, hashedPassword) return this.getUser(userId)! diff --git a/src/routes/api/jellyfin/auth/+server.ts b/src/routes/api/jellyfin/auth/+server.ts deleted file mode 100644 index 5443c82..0000000 --- a/src/routes/api/jellyfin/auth/+server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { RequestHandler } from '@sveltejs/kit' -import { isValidURL } from '$lib/utils' -import { z } from 'zod' - -export const POST: RequestHandler = async ({ request, fetch }) => { - const jellyfinAuthSchema = z.object({ - serverUrl: z.string().refine((val) => isValidURL(val)), - username: z.string(), - password: z.string(), - deviceId: z.string(), - }) - - const jellyfinAuthData = await request.json() - const jellyfinAuthValidation = jellyfinAuthSchema.safeParse(jellyfinAuthData) - if (!jellyfinAuthValidation.success) return new Response('Invalid data in request body', { status: 400 }) - - const { serverUrl, username, password, deviceId } = jellyfinAuthValidation.data - const authUrl = new URL('/Users/AuthenticateByName', serverUrl).href - try { - const authResponse = await fetch(authUrl, { - method: 'POST', - body: JSON.stringify({ - Username: username, - Pw: password, - }), - headers: { - 'Content-Type': 'application/json; charset=utf-8', - 'X-Emby-Authorization': `MediaBrowser Client="Lazuli", Device="Chrome", DeviceId="${deviceId}", Version="1.0.0.0"`, - }, - }) - if (!authResponse.ok) return new Response('Failed to authenticate', { status: 401 }) - - const authData = await authResponse.json() - return Response.json({ - userId: authData.User.Id, - accessToken: authData.AccessToken, - }) - } catch { - return new Response('Fetch request failed', { status: 404 }) - } -} diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts index 27a56f4..7e3f99f 100644 --- a/src/routes/login/+page.server.ts +++ b/src/routes/login/+page.server.ts @@ -15,10 +15,10 @@ export const actions: Actions = { const formData = await request.formData() const { username, password, redirectLocation } = Object.fromEntries(formData) - const user = Users.getUsername(username.toString(), { includePassword: true }) + const user = Users.getUsername(username.toString()) if (!user) return fail(400, { message: 'Invalid Username' }) - const passwordValid = await compare(password.toString(), user.password!) + const passwordValid = await compare(password.toString(), user.password) if (!passwordValid) return fail(400, { message: 'Invalid Password' }) const authToken = jwt.sign({ id: user.id, username: user.username }, SECRET_JWT_KEY, { expiresIn: '100d' }) @@ -33,14 +33,11 @@ export const actions: Actions = { const formData = await request.formData() const { username, password } = Object.fromEntries(formData) - const existingUsers = Users.allUsers() - const existingUsernames = Array.from(existingUsers, (user) => user.username) - if (username.toString() in existingUsernames) return fail(400, { message: 'Username already in use' }) - const passwordHash = await hash(password.toString(), 10) const newUser = Users.addUser(username.toString(), passwordHash) + if (!newUser) return fail(400, { message: 'Username already in use' }) - const authToken = jwt.sign(newUser, SECRET_JWT_KEY, { expiresIn: '100d' }) + const authToken = jwt.sign({ id: newUser.id, username: newUser.username }, SECRET_JWT_KEY, { expiresIn: '100d' }) cookies.set('lazuli-auth', authToken, { path: '/', httpOnly: true, sameSite: 'strict', secure: false, maxAge: 60 * 60 * 24 * 100 }) diff --git a/src/routes/settings/connections/+page.server.ts b/src/routes/settings/connections/+page.server.ts index 906520e..94de70b 100644 --- a/src/routes/settings/connections/+page.server.ts +++ b/src/routes/settings/connections/+page.server.ts @@ -12,6 +12,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => { }) const userConnections = await connectionsResponse.json() + return { connections: userConnections.connections } }