basic db boilerplate + type safety for api
This commit is contained in:
parent
b91e72626f
commit
c5686166d7
15 changed files with 269 additions and 12 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
11
src/hooks.server.ts
Normal file
11
src/hooks.server.ts
Normal file
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
});
|
4
src/lib/server/repo/index.ts
Normal file
4
src/lib/server/repo/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { db } from "../db";
|
||||
import { SuyuUser } from "../schema";
|
||||
|
||||
export const userRepo = db.getRepository(SuyuUser);
|
|
@ -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;
|
||||
}
|
||||
|
|
17
src/lib/server/util/index.ts
Normal file
17
src/lib/server/util/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { Role } from "$types/db";
|
||||
|
||||
export function json<T>(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[];
|
||||
}
|
15
src/lib/util/api/index.ts
Normal file
15
src/lib/util/api/index.ts
Normal file
|
@ -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<SuyuUser | null> {
|
||||
const apiKey = typeof request === "string" ? request : request.headers.get("Authorization");
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
apiKey,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
</script>
|
||||
|
||||
{#if !isNavExcluded}
|
||||
<div class="header">
|
||||
<div class="header panel-blur">
|
||||
<div class="left">
|
||||
<LogoWithTextHorizontal on:click={() => goto("/")} size={50} />
|
||||
</div>
|
||||
|
@ -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;
|
||||
|
|
10
src/routes/account/+page.server.ts
Normal file
10
src/routes/account/+page.server.ts
Normal file
|
@ -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",
|
||||
};
|
||||
}
|
68
src/routes/account/+page.svelte
Normal file
68
src/routes/account/+page.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { SuyuAPI } from "$lib/client/api";
|
||||
import type { CreateAccountResponse } from "$types/api";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let usernameToCreate: string;
|
||||
|
||||
async function createAccount() {
|
||||
const response = await SuyuAPI.users.createAccount({ username: usernameToCreate });
|
||||
if (response.success) {
|
||||
data = {
|
||||
...(data || {}),
|
||||
user: response.user,
|
||||
};
|
||||
// add api_key cookie
|
||||
document.cookie = `api_key=${response.token}; path=/`;
|
||||
} else {
|
||||
alert("Failed to create account: " + response.error);
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="panel-blur main-panel">
|
||||
<h2>Account Settings</h2>
|
||||
<p>
|
||||
{#if data?.user}
|
||||
<p>Username: {data.user.username}</p>
|
||||
{:else}
|
||||
<p>
|
||||
It appears you don't have an account; please register one to access suyu's online
|
||||
services.
|
||||
</p>
|
||||
<div class="create-account">
|
||||
<input bind:value={usernameToCreate} type="text" placeholder="Username" />
|
||||
<button on:click={createAccount}>Create Account</button>
|
||||
</div>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main-panel {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, calc(-50% + 40px));
|
||||
width: calc(100% - 120px);
|
||||
height: calc(100% - 240px);
|
||||
max-height: 600px;
|
||||
min-height: 400px;
|
||||
max-width: 1000px;
|
||||
padding: 28px 36px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.main-panel > h2 {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.create-account {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
45
src/routes/api/user/create-account/+server.ts
Normal file
45
src/routes/api/user/create-account/+server.ts
Normal file
|
@ -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<CreateAccountResponse>({
|
||||
success: false,
|
||||
error: "username is required",
|
||||
});
|
||||
}
|
||||
// check if user exists
|
||||
const user = await userRepo.findOne({
|
||||
where: {
|
||||
username: body.username,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
return json<CreateAccountResponse>({
|
||||
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<CreateAccountResponse>({
|
||||
success: true,
|
||||
token: createdUser.apiKey,
|
||||
user: createdUser,
|
||||
});
|
||||
}
|
17
src/routes/api/user/self/+server.ts
Normal file
17
src/routes/api/user/self/+server.ts
Normal file
|
@ -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<GetUserResponse>({
|
||||
success: false,
|
||||
error: "unauthorized",
|
||||
});
|
||||
}
|
||||
return json<GetUserResponse>({
|
||||
success: true,
|
||||
user,
|
||||
});
|
||||
}
|
12
src/types/api.d.ts
vendored
12
src/types/api.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue