Started playing around with typescript generic types
This commit is contained in:
5
src/app.d.ts
vendored
5
src/app.d.ts
vendored
@@ -4,7 +4,7 @@ declare global {
|
|||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
interface Locals {
|
interface Locals {
|
||||||
user: User
|
user: Omit<User, 'password'>
|
||||||
}
|
}
|
||||||
// interface PageData {}
|
// interface PageData {}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
@@ -21,7 +21,7 @@ declare global {
|
|||||||
interface User {
|
interface User {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
password: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type serviceType = 'jellyfin' | 'youtube-music'
|
type serviceType = 'jellyfin' | 'youtube-music'
|
||||||
@@ -39,6 +39,7 @@ declare global {
|
|||||||
accessToken: string
|
accessToken: string
|
||||||
refreshToken?: string
|
refreshToken?: string
|
||||||
expiry?: number
|
expiry?: number
|
||||||
|
connectionInfo?: ConnectionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConnectionInfo {
|
interface ConnectionInfo {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
if (!authToken) throw redirect(303, `/login?redirect=${urlpath}`)
|
if (!authToken) throw redirect(303, `/login?redirect=${urlpath}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokenData = jwt.verify(authToken, SECRET_JWT_KEY) as User
|
const tokenData = jwt.verify(authToken, SECRET_JWT_KEY) as Omit<User, 'password'>
|
||||||
event.locals.user = tokenData
|
event.locals.user = tokenData
|
||||||
} catch {
|
} catch {
|
||||||
throw redirect(303, `/login?redirect=${urlpath}`)
|
throw redirect(303, `/login?redirect=${urlpath}`)
|
||||||
|
|||||||
Binary file not shown.
@@ -1,10 +1,14 @@
|
|||||||
import Database from 'better-sqlite3'
|
import Database, { SqliteError } from 'better-sqlite3'
|
||||||
import { generateUUID } from '$lib/utils'
|
import { generateUUID } from '$lib/utils'
|
||||||
import { isValidURL } from '$lib/utils'
|
import { isValidURL } from '$lib/utils'
|
||||||
|
|
||||||
const db = new Database('./src/lib/server/users.db', { verbose: console.info })
|
const db = new Database('./src/lib/server/users.db', { verbose: console.info })
|
||||||
db.pragma('foreign_keys = ON')
|
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(
|
const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections(
|
||||||
id VARCHAR(36) PRIMARY KEY,
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
userId VARCHAR(36) NOT NULL,
|
userId VARCHAR(36) NOT NULL,
|
||||||
@@ -16,10 +20,6 @@ const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections(
|
|||||||
)`
|
)`
|
||||||
db.exec(initUsersTable), db.exec(initConnectionsTable)
|
db.exec(initUsersTable), db.exec(initConnectionsTable)
|
||||||
|
|
||||||
type UserQueryParams = {
|
|
||||||
includePassword?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConnectionsTableSchema {
|
interface ConnectionsTableSchema {
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
@@ -30,25 +30,19 @@ interface ConnectionsTableSchema {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Users {
|
export class Users {
|
||||||
static getUser = (id: string, params: UserQueryParams | null = null): User | undefined => {
|
static getUser = (id: string): User | null => {
|
||||||
const user = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User | undefined
|
const user = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User | null
|
||||||
if (user && !params?.includePassword) delete user.password
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
static getUsername = (username: string, params: UserQueryParams | null = null): User | undefined => {
|
static getUsername = (username: string): User | null => {
|
||||||
const user = db.prepare('SELECT * FROM Users WHERE lower(username) = ?').get(username.toLowerCase()) as User | undefined
|
const user = db.prepare('SELECT * FROM Users WHERE lower(username) = ?').get(username.toLowerCase()) as User | null
|
||||||
if (user && !params?.includePassword) delete user.password
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
static allUsers = (includePassword: boolean = false): User[] => {
|
static addUser = (username: string, hashedPassword: string): User | null => {
|
||||||
const users = db.prepare('SELECT * FROM Users').all() as User[]
|
if (this.getUsername(username)) return null
|
||||||
if (!includePassword) users.forEach((user) => delete user.password)
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
|
|
||||||
static addUser = (username: string, hashedPassword: string): User => {
|
|
||||||
const userId = generateUUID()
|
const userId = generateUUID()
|
||||||
db.prepare('INSERT INTO Users(id, username, password) VALUES(?, ?, ?)').run(userId, username, hashedPassword)
|
db.prepare('INSERT INTO Users(id, username, password) VALUES(?, ?, ?)').run(userId, username, hashedPassword)
|
||||||
return this.getUser(userId)!
|
return this.getUser(userId)!
|
||||||
|
|||||||
@@ -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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,10 +15,10 @@ export const actions: Actions = {
|
|||||||
const formData = await request.formData()
|
const formData = await request.formData()
|
||||||
const { username, password, redirectLocation } = Object.fromEntries(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' })
|
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' })
|
if (!passwordValid) return fail(400, { message: 'Invalid Password' })
|
||||||
|
|
||||||
const authToken = jwt.sign({ id: user.id, username: user.username }, SECRET_JWT_KEY, { expiresIn: '100d' })
|
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 formData = await request.formData()
|
||||||
const { username, password } = Object.fromEntries(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 passwordHash = await hash(password.toString(), 10)
|
||||||
const newUser = Users.addUser(username.toString(), passwordHash)
|
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 })
|
cookies.set('lazuli-auth', authToken, { path: '/', httpOnly: true, sameSite: 'strict', secure: false, maxAge: 60 * 60 * 24 * 100 })
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const load: PageServerLoad = async ({ fetch, locals }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const userConnections = await connectionsResponse.json()
|
const userConnections = await connectionsResponse.json()
|
||||||
|
|
||||||
return { connections: userConnections.connections }
|
return { connections: userConnections.connections }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user