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

2
src/app.d.ts vendored
View File

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

View File

@@ -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

View File

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

View File

@@ -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`)
}
}

View File

@@ -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)

View File

@@ -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, '/')
},
}

View File

@@ -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>