DB looks good, need to test login page

This commit is contained in:
Eclypsed
2024-01-25 03:05:13 -05:00
parent e86b103af0
commit 0ad1ace45b
9 changed files with 212 additions and 31 deletions

109
package-lock.json generated
View File

@@ -10,8 +10,10 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-free": "^6.5.1",
"@types/better-sqlite3": "^7.6.8", "@types/better-sqlite3": "^7.6.8",
"@types/jsonwebtoken": "^9.0.5",
"bcrypt-ts": "^5.0.1", "bcrypt-ts": "^5.0.1",
"better-sqlite3": "^9.3.0" "better-sqlite3": "^9.3.0",
"jsonwebtoken": "^9.0.2"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
@@ -730,9 +732,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.4.1", "version": "2.4.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.4.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.4.3.tgz",
"integrity": "sha512-NnDrPOmTjzhgWkwJNPcth3vBMWQmI/QhwbMRXow1p/RkM+17HxP2yQR3GYwIK83rkYSKwQiweyBVWGOjJY4gsg==", "integrity": "sha512-nKNhUdt61vtD961kQpUk6vLDhpnV0yku5F1uYNWvrJYFV0+cGfmW7ol0JVMSjHMXlMtmmv2FTc+nPRrTFwb2UA==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@@ -820,6 +822,14 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz",
"integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.5", "version": "20.11.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
@@ -1106,6 +1116,11 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1368,6 +1383,14 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true "dev": true
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.4.640", "version": "1.4.640",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz",
@@ -1806,6 +1829,46 @@
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kleur": { "node_modules/kleur": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -1836,6 +1899,41 @@
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true "dev": true
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
@@ -1972,8 +2070,7 @@
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
"dev": true
}, },
"node_modules/mz": { "node_modules/mz": {
"version": "2.7.0", "version": "2.7.0",

View File

@@ -29,7 +29,9 @@
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^6.5.1", "@fortawesome/fontawesome-free": "^6.5.1",
"@types/better-sqlite3": "^7.6.8", "@types/better-sqlite3": "^7.6.8",
"@types/jsonwebtoken": "^9.0.5",
"bcrypt-ts": "^5.0.1", "bcrypt-ts": "^5.0.1",
"better-sqlite3": "^9.3.0" "better-sqlite3": "^9.3.0",
"jsonwebtoken": "^9.0.2"
} }
} }

2
src/app.d.ts vendored
View File

@@ -8,8 +8,6 @@ declare global {
// interface PageState {} // interface PageState {}
// interface Platform {} // interface Platform {}
} }
type AlertType = 'info' | 'success' | 'warning' | 'caution'
} }
export {} export {}

View File

@@ -1,3 +1,7 @@
<script context="module" lang="ts">
export type AlertType = 'info' | 'success' | 'warning' | 'caution'
</script>
<script lang="ts"> <script lang="ts">
export let alertType: AlertType export let alertType: AlertType
export let alertMessage: string export let alertMessage: string

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Alert from './alert.svelte' import Alert from './alert.svelte'
import type { AlertType } from './alert.svelte'
let alertBox: HTMLDivElement let alertBox: HTMLDivElement
let alertQueue: Alert[] = [] let alertQueue: Alert[] = []

View File

@@ -11,17 +11,21 @@ db.exec(initUsersTable)
db.exec(initServicesTable) db.exec(initServicesTable)
db.exec(initConnectionsTable) db.exec(initConnectionsTable)
export interface User { interface User {
id: string id: string
username: string username: string
password: string password?: string
} }
export type serviceType = 'jellyfin' | 'youtube-music' type UserQueryParams = {
includePassword?: boolean
}
export interface Service { type ServiceType = 'jellyfin' | 'youtube-music'
interface Service {
id: string id: string
type: serviceType type: ServiceType
userId: string userId: string
url: URL url: URL
} }
@@ -33,7 +37,7 @@ interface DBServiceRow {
url: string url: string
} }
export interface Connection { interface Connection {
id: string id: string
user: User user: User
service: Service service: Service
@@ -47,46 +51,86 @@ interface DBConnectionRow {
userId: string userId: string
serviceId: string serviceId: string
accessToken: string accessToken: string
refreshToken: string refreshToken: string | null
expiry: number expiry: number | null
} }
export class Users { export class Users {
static getUser = (id: string): User => { static getUser = (id: string, params: UserQueryParams | null = null): User | undefined => {
return db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User const user = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User | undefined
if (user && !params?.includePassword) delete user.password
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
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 => { 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)!
}
static deleteUser = (id: string): void => {
const commandInfo = db.prepare('DELETE FROM Users WHERE id = ?').run(id)
if (commandInfo.changes === 0) throw new Error(`User with id ${id} does not exist`)
} }
} }
export class Services { export class Services {
static getService = (id: string): Service => { static getService = (id: string): Service => {
const { type, userId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow const { type, userId, url } = db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as DBServiceRow
const service: Service = { id, type: type as serviceType, userId, url: new URL(url) } const service: Service = { id, type: type as ServiceType, userId, url: new URL(url) }
return service return service
} }
static addService = (type: serviceType, userId: string, url: URL): Service => { static addService = (type: ServiceType, userId: string, url: URL): Service => {
const serviceId = generateUUID() const serviceId = generateUUID()
db.prepare('INSERT INTO Services(id, type, userId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, userId, url.origin) db.prepare('INSERT INTO Services(id, type, userId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, userId, url.origin)
return this.getService(serviceId) return this.getService(serviceId)
} }
static deleteService = (id: string): void => {
const commandInfo = db.prepare('DELETE FROM Services WHERE id = ?').run(id)
if (commandInfo.changes === 0) throw new Error(`Serivce with id ${id} does not exist`)
}
} }
export class Connections { export class Connections {
static getConnection = (id: string): Connection => { static getConnection = (id: string): Connection => {
const { userId, serviceId, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as DBConnectionRow const { userId, serviceId, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as DBConnectionRow
const connection: Connection = { id, user: Users.getUser(userId), service: Services.getService(serviceId), accessToken, refreshToken, expiry } const connection: Connection = { id, user: Users.getUser(userId)!, service: Services.getService(serviceId), accessToken, refreshToken, expiry }
return connection return connection
} }
static getUserConnections = (userId: string): Connection[] => {
const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as DBConnectionRow[]
const connections: Connection[] = []
const user = Users.getUser(userId)!
connectionRows.forEach((row) => {
const { id, serviceId, accessToken, refreshToken, expiry } = row
connections.push({ id, user, service: Services.getService(serviceId), accessToken, refreshToken, expiry })
})
return connections
}
static addConnection = (userId: string, serviceId: string, accessToken: string, refreshToken: string | null, expiry: number | null): Connection => { static addConnection = (userId: string, serviceId: string, accessToken: string, refreshToken: string | null, expiry: number | null): Connection => {
const connectionId = generateUUID() const connectionId = generateUUID()
db.prepare('INSERT INTO Connections(id, userId, serviceId, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, serviceId, accessToken, refreshToken, expiry) db.prepare('INSERT INTO Connections(id, userId, serviceId, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, serviceId, accessToken, refreshToken, expiry)
return this.getConnection(connectionId) return this.getConnection(connectionId)
} }
static deleteConnection = (id: string): void => {
const commandInfo = db.prepare('DELETE FROM Connections WHERE id = ?').run(id)
if (commandInfo.changes === 0) throw new Error(`Connection with id: ${id} does not exist`)
}
} }

View File

@@ -1,5 +1,5 @@
import { writable } from 'svelte/store' import { writable, type Writable } from 'svelte/store'
import type { Writable } from 'svelte/store' import type { AlertType } from '$lib/components/util/alert.svelte'
export const pageWidth: Writable<number> = writable(0) export const pageWidth: Writable<number> = writable(0)

View File

@@ -1,9 +1,9 @@
import { SECRET_JWT_KEY } from '$env/static/private' import { SECRET_JWT_KEY } from '$env/static/private'
import { fail, redirect } from '@sveltejs/kit' import { fail, redirect } from '@sveltejs/kit'
import { genSaltSync, hashSync } from 'bcrypt-ts' import { compare, hash } from 'bcrypt-ts'
import type { PageServerLoad, Actions } from './$types' import type { PageServerLoad, Actions } from './$types'
import { Users } from '$lib/server/users' import { Users } from '$lib/server/users'
import type { User } from '$lib/server/users' import { sign } from 'jsonwebtoken'
export const load: PageServerLoad = async ({ url }) => { export const load: PageServerLoad = async ({ url }) => {
const redirectLocation = url.searchParams.get('redirect') const redirectLocation = url.searchParams.get('redirect')
@@ -14,5 +14,36 @@ export const actions: Actions = {
signIn: async ({ request, cookies }) => { signIn: async ({ request, cookies }) => {
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 })
if (!user) return fail(400, { message: 'Invalid Username' })
const passwordValid = await compare(password.toString(), user.password!)
if (!passwordValid) return fail(400, { message: 'Invalid Password' })
const authToken = sign({ id: user.id, username: user.username }, SECRET_JWT_KEY, { expiresIn: '100d' })
cookies.set('lazuli-auth', authToken, { path: '/', httpOnly: true, sameSite: 'strict', secure: false, maxAge: 60 * 60 * 24 * 100 })
if (redirectLocation) throw redirect(303, redirectLocation.toString())
throw redirect(303, '/')
},
newUser: async ({ request, cookies }) => {
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)
const authToken = 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 })
throw redirect(303, '/')
}, },
} }

View File

@@ -3,10 +3,10 @@
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
import { newestAlert } from '$lib/stores' import { newestAlert } from '$lib/stores'
import type { PageServerData } from '../$types' import type { PageData } from './$types'
import type { SubmitFunction } from '@sveltejs/kit' import type { SubmitFunction } from '@sveltejs/kit'
// export let data: PageServerData export let data: PageData
type FormMode = 'signIn' | 'newUser' type FormMode = 'signIn' | 'newUser'
let formMode: FormMode = 'signIn' let formMode: FormMode = 'signIn'
@@ -42,8 +42,12 @@
} }
} }
console.log('Passed all checks') if (data.redirectLocation) formData.append('redirectLocation', data.redirectLocation)
cancel()
return async ({ result }) => {
if (result.type === 'failure') return ($newestAlert = ['warning', result.data?.message])
if (result.type === 'redirect') return goto(result.location)
}
} }
</script> </script>