DB looks good, need to test login page
This commit is contained in:
109
package-lock.json
generated
109
package-lock.json
generated
@@ -10,8 +10,10 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"bcrypt-ts": "^5.0.1",
|
||||
"better-sqlite3": "^9.3.0"
|
||||
"better-sqlite3": "^9.3.0",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
@@ -730,9 +732,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.4.1.tgz",
|
||||
"integrity": "sha512-NnDrPOmTjzhgWkwJNPcth3vBMWQmI/QhwbMRXow1p/RkM+17HxP2yQR3GYwIK83rkYSKwQiweyBVWGOjJY4gsg==",
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.4.3.tgz",
|
||||
"integrity": "sha512-nKNhUdt61vtD961kQpUk6vLDhpnV0yku5F1uYNWvrJYFV0+cGfmW7ol0JVMSjHMXlMtmmv2FTc+nPRrTFwb2UA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
@@ -820,6 +822,14 @@
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"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": {
|
||||
"version": "20.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz",
|
||||
@@ -1106,6 +1116,11 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -1368,6 +1383,14 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"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": {
|
||||
"version": "1.4.640",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz",
|
||||
@@ -1806,6 +1829,46 @@
|
||||
"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": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
@@ -1836,6 +1899,41 @@
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"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": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz",
|
||||
@@ -1972,8 +2070,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mz": {
|
||||
"version": "2.7.0",
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"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
2
src/app.d.ts
vendored
@@ -8,8 +8,6 @@ declare global {
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
type AlertType = 'info' | 'success' | 'warning' | 'caution'
|
||||
}
|
||||
|
||||
export {}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<script context="module" lang="ts">
|
||||
export type AlertType = 'info' | 'success' | 'warning' | 'caution'
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export let alertType: AlertType
|
||||
export let alertMessage: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Alert from './alert.svelte'
|
||||
import type { AlertType } from './alert.svelte'
|
||||
|
||||
let alertBox: HTMLDivElement
|
||||
let alertQueue: Alert[] = []
|
||||
|
||||
@@ -11,17 +11,21 @@ db.exec(initUsersTable)
|
||||
db.exec(initServicesTable)
|
||||
db.exec(initConnectionsTable)
|
||||
|
||||
export interface User {
|
||||
interface User {
|
||||
id: 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
|
||||
type: serviceType
|
||||
type: ServiceType
|
||||
userId: string
|
||||
url: URL
|
||||
}
|
||||
@@ -33,7 +37,7 @@ interface DBServiceRow {
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface Connection {
|
||||
interface Connection {
|
||||
id: string
|
||||
user: User
|
||||
service: Service
|
||||
@@ -47,46 +51,86 @@ interface DBConnectionRow {
|
||||
userId: string
|
||||
serviceId: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiry: number
|
||||
refreshToken: string | null
|
||||
expiry: number | null
|
||||
}
|
||||
|
||||
export class Users {
|
||||
static getUser = (id: string): User => {
|
||||
return db.prepare('SELECT * FROM Users WHERE id = ?').get(id) as User
|
||||
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
|
||||
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 => {
|
||||
const userId = generateUUID()
|
||||
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 {
|
||||
static getService = (id: string): Service => {
|
||||
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
|
||||
}
|
||||
|
||||
static addService = (type: serviceType, userId: string, url: URL): Service => {
|
||||
static addService = (type: ServiceType, userId: string, url: URL): Service => {
|
||||
const serviceId = generateUUID()
|
||||
db.prepare('INSERT INTO Services(id, type, userId, url) VALUES(?, ?, ?, ?)').run(serviceId, type, userId, url.origin)
|
||||
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 {
|
||||
static getConnection = (id: string): Connection => {
|
||||
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
|
||||
}
|
||||
|
||||
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 => {
|
||||
const connectionId = generateUUID()
|
||||
db.prepare('INSERT INTO Connections(id, userId, serviceId, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, serviceId, accessToken, refreshToken, expiry)
|
||||
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`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import type { AlertType } from '$lib/components/util/alert.svelte'
|
||||
|
||||
export const pageWidth: Writable<number> = writable(0)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { SECRET_JWT_KEY } from '$env/static/private'
|
||||
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 { Users } from '$lib/server/users'
|
||||
import type { User } from '$lib/server/users'
|
||||
import { sign } from 'jsonwebtoken'
|
||||
|
||||
export const load: PageServerLoad = async ({ url }) => {
|
||||
const redirectLocation = url.searchParams.get('redirect')
|
||||
@@ -14,5 +14,36 @@ export const actions: Actions = {
|
||||
signIn: async ({ request, cookies }) => {
|
||||
const formData = await request.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, '/')
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import { goto } from '$app/navigation'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { newestAlert } from '$lib/stores'
|
||||
import type { PageServerData } from '../$types'
|
||||
import type { PageData } from './$types'
|
||||
import type { SubmitFunction } from '@sveltejs/kit'
|
||||
|
||||
// export let data: PageServerData
|
||||
export let data: PageData
|
||||
|
||||
type FormMode = 'signIn' | 'newUser'
|
||||
let formMode: FormMode = 'signIn'
|
||||
@@ -42,8 +42,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Passed all checks')
|
||||
cancel()
|
||||
if (data.redirectLocation) formData.append('redirectLocation', data.redirectLocation)
|
||||
|
||||
return async ({ result }) => {
|
||||
if (result.type === 'failure') return ($newestAlert = ['warning', result.data?.message])
|
||||
if (result.type === 'redirect') return goto(result.location)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user