diff --git a/package-lock.json b/package-lock.json index b1045ed..bc09be8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/enhanced-img": "^0.1.8", "better-sqlite3": "^9.4.3", + "reflect-metadata": "^0.2.1", "sequelize": "^6.37.1", "sqlite3": "^5.1.7", "typeorm": "^0.3.20", diff --git a/package.json b/package.json index 745e325..95d7a7a 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/enhanced-img": "^0.1.8", "better-sqlite3": "^9.4.3", + "reflect-metadata": "^0.2.1", "sequelize": "^6.37.1", "sqlite3": "^5.1.7", "typeorm": "^0.3.20", diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..780214d --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,11 @@ +import { db } from "$lib/server/db"; +import "reflect-metadata"; +import { building } from "$app/environment"; + +const runAllTheInitFunctions = async () => { + await db.initialize(); +}; + +if (!building) { + await runAllTheInitFunctions(); +} diff --git a/src/lib/css/index.css b/src/lib/css/index.css index bbef2a0..58caecb 100644 --- a/src/lib/css/index.css +++ b/src/lib/css/index.css @@ -25,7 +25,30 @@ h4, h5, h6 { font-family: var(--header-font) !important; - font-size: 64px; +} + +h1 { + font-size: 48px; +} + +h2 { + font-size: 36px; +} + +h3 { + font-size: 24px; +} + +h4 { + font-size: 18px; +} + +h5 { + font-size: 16px; +} + +h6 { + font-size: 14px; } body { @@ -70,3 +93,26 @@ button:active { transition: 0.05s ease-in-out filter; filter: brightness(0.9); } + +.panel-blur { + background: color-mix(in srgb, var(--color-primary), transparent 50%); + border: var(--border-primary); + border-radius: 16px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: 0 0 48px 0 rgba(39, 56, 75, 0.5); +} + +input[type="text"], +input[type="password"] { + padding: 8px 16px; + background-color: var(--color-primary); + border: var(--border-primary); + border-radius: 16px; + color: var(--text-color); +} + +input[type="text"]:focus, +input[type="password"]:focus { + outline: solid 2px var(--text-color); +} diff --git a/src/lib/server/schema/db/index.ts b/src/lib/server/db/index.ts similarity index 52% rename from src/lib/server/schema/db/index.ts rename to src/lib/server/db/index.ts index 84afed8..c83d241 100644 --- a/src/lib/server/schema/db/index.ts +++ b/src/lib/server/db/index.ts @@ -1,6 +1,11 @@ import { DataSource } from "typeorm"; +import { SuyuUser } from "../schema"; export const db = new DataSource({ type: "better-sqlite3", database: "db.sqlite", + entities: [SuyuUser], + synchronize: true, + subscribers: [], + migrations: [], }); diff --git a/src/lib/server/repo/index.ts b/src/lib/server/repo/index.ts new file mode 100644 index 0000000..2aefad5 --- /dev/null +++ b/src/lib/server/repo/index.ts @@ -0,0 +1,4 @@ +import { db } from "../db"; +import { SuyuUser } from "../schema"; + +export const userRepo = db.getRepository(SuyuUser); diff --git a/src/lib/server/schema/index.ts b/src/lib/server/schema/index.ts index 4636d3b..c7f2474 100644 --- a/src/lib/server/schema/index.ts +++ b/src/lib/server/schema/index.ts @@ -1,20 +1,25 @@ import type { Role } from "$types/db"; -import { Column, Entity, PrimaryColumn } from "typeorm"; +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; @Entity() -export class SuyuUser { - @PrimaryColumn() +export class SuyuUser extends BaseEntity { + @PrimaryGeneratedColumn("uuid") id: string; - @Column() + @Column("text") username: string; - @Column() + @Column("text") displayName: string; - @Column() + @Column("text") avatarUrl: string; - @Column() - roles: Role[]; + @Column("text") + roles: string; + + @Column("text", { + select: false, + }) + apiKey: string; } diff --git a/src/lib/server/util/index.ts b/src/lib/server/util/index.ts new file mode 100644 index 0000000..439e7b7 --- /dev/null +++ b/src/lib/server/util/index.ts @@ -0,0 +1,17 @@ +import type { Role } from "$types/db"; + +export function json(body: T): Response { + return new Response(JSON.stringify(body), { + headers: { + "content-type": "application/json;charset=UTF-8", + }, + }); +} + +export function serializeRoles(roles: Role[]): string { + return roles.join("|"); +} + +export function deserializeRoles(roles: string): Role[] { + return roles.split("|") as Role[]; +} diff --git a/src/lib/util/api/index.ts b/src/lib/util/api/index.ts new file mode 100644 index 0000000..f490364 --- /dev/null +++ b/src/lib/util/api/index.ts @@ -0,0 +1,15 @@ +import { userRepo } from "$lib/server/repo"; +import type { SuyuUser } from "$lib/server/schema"; + +export async function useAuth(request: Request | string): Promise { + const apiKey = typeof request === "string" ? request : request.headers.get("Authorization"); + if (!apiKey) { + return null; + } + const user = await userRepo.findOne({ + where: { + apiKey, + }, + }); + return user; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f813bce..3866747 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -16,7 +16,7 @@ {#if !isNavExcluded} -
+
goto("/")} size={50} />
@@ -66,10 +66,10 @@ width: 100%; height: 80px; background-color: color-mix(in srgb, var(--color-primary), transparent 50%); + border: none; + border-radius: 0; border-bottom: var(--border-primary); z-index: 1000; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); align-items: center; display: flex; padding: 0 32px 0 16px; diff --git a/src/routes/account/+page.server.ts b/src/routes/account/+page.server.ts new file mode 100644 index 0000000..dbc3846 --- /dev/null +++ b/src/routes/account/+page.server.ts @@ -0,0 +1,10 @@ +import { useAuth } from "$lib/util/api"; + +export async function load(opts) { + const apiKey = opts.cookies.get("api_key"); + const user = await useAuth(apiKey || "unused"); + return { + user: { ...user }, + a: "B", + }; +} diff --git a/src/routes/account/+page.svelte b/src/routes/account/+page.svelte new file mode 100644 index 0000000..fb19204 --- /dev/null +++ b/src/routes/account/+page.svelte @@ -0,0 +1,68 @@ + + +
+

Account Settings

+

+ {#if data?.user} +

Username: {data.user.username}

+ {:else} +

+ It appears you don't have an account; please register one to access suyu's online + services. +

+ + {/if} +

+
+ + diff --git a/src/routes/api/user/create-account/+server.ts b/src/routes/api/user/create-account/+server.ts new file mode 100644 index 0000000..c0e4936 --- /dev/null +++ b/src/routes/api/user/create-account/+server.ts @@ -0,0 +1,45 @@ +// TODO: refactor into external utils (ie Suyu.createAccount() or something???) + +import { userRepo } from "$lib/server/repo"; +import type { SuyuUser } from "$lib/server/schema"; +import { json, serializeRoles } from "$lib/server/util"; +import type { CreateAccountRequest, CreateAccountResponse } from "$types/api"; +import crypto from "crypto"; +import { promisify } from "util"; + +const randomBytes = promisify(crypto.randomBytes); + +export async function POST({ request }) { + const body: CreateAccountRequest = await request.json(); + if (!body.username) { + return json({ + success: false, + error: "username is required", + }); + } + // check if user exists + const user = await userRepo.findOne({ + where: { + username: body.username, + }, + }); + if (user) { + return json({ + success: false, + error: "username already exists", + }); + } + const createdUser: SuyuUser = userRepo.create({ + username: body.username, + avatarUrl: `https://avatars.githubusercontent.com/u/${Math.floor(Math.random() * 100000000)}?v=4`, + displayName: body.username, + roles: serializeRoles(["user"]), + apiKey: `${body.username}:${(await randomBytes(32)).toString("hex")}`, + }); + await userRepo.save(createdUser); + return json({ + success: true, + token: createdUser.apiKey, + user: createdUser, + }); +} diff --git a/src/routes/api/user/self/+server.ts b/src/routes/api/user/self/+server.ts new file mode 100644 index 0000000..2769915 --- /dev/null +++ b/src/routes/api/user/self/+server.ts @@ -0,0 +1,17 @@ +import { json } from "$lib/server/util"; +import { useAuth } from "$lib/util/api"; +import type { GetUserResponse } from "$types/api"; + +export async function GET({ request }) { + const user = await useAuth(request); + if (!user) { + return json({ + success: false, + error: "unauthorized", + }); + } + return json({ + success: true, + user, + }); +} diff --git a/src/types/api.d.ts b/src/types/api.d.ts index d39a56a..383139d 100644 --- a/src/types/api.d.ts +++ b/src/types/api.d.ts @@ -18,3 +18,15 @@ export interface CreateAccountResponseFailure { } export type CreateAccountResponse = CreateAccountResponseSuccess | CreateAccountResponseFailure; + +export interface GetUserResponseSuccess { + success: true; + user: SuyuUser; +} + +export interface GetUserResponseFailure { + success: false; + error: string; +} + +export type GetUserResponse = GetUserResponseSuccess | GetUserResponseFailure;