Just working with a DB and stuff

This commit is contained in:
Sandra Modzelewska 2025-01-29 18:59:31 +00:00
parent b8c555d8e7
commit 32281aa1ff
No known key found for this signature in database
23 changed files with 1962 additions and 183 deletions

5
.env Normal file
View File

@ -0,0 +1,5 @@
DATABASE_USER=kredens
DATABASE_NAME=kredens
DATABASE_PASSWORD=
DATABASE_HOST=localhost
DATABASE_URL=postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}

2
.env.development Normal file
View File

@ -0,0 +1,2 @@
DATABASE_USER=postgres
DATABASE_HOST=database

5
.gitignore vendored
View File

@ -14,10 +14,7 @@ node_modules
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
.env*.local
# Vite
vite.config.js.timestamp-*

13
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"server": "database",
"port": 5432,
"driver": "PostgreSQL",
"name": "Dev Container Backend",
"database": "kredens",
"username": "postgres"
}
]
}

View File

@ -9,10 +9,10 @@ sudo chown $(id -u):$(id -g) -fR \
cd ${WORKSPACE_FOLDER}
if [ ! -f .env ]; then
if [ ! -f .env.local ]; then
DATABASE_PASSWORD=$(pwgen 16 1)
SUPERUSER_PASSWORD=$(pwgen 8 1)
cat << EOF > .env
cat << EOF > .env.local
DATABASE_PASSWORD=${DATABASE_PASSWORD}
SUPERUSER_EMAIL=super@example.org

21
drizzle.config.ts Normal file
View File

@ -0,0 +1,21 @@
import dotenv from "dotenv";
import dotenvExpand from "dotenv-expand";
import { defineConfig } from "drizzle-kit";
dotenvExpand.expand(
dotenv.config({
path: [".env" , ".env.local", ".env.development", ".env.development.local"],
override: true
})
);
export default defineConfig({
out: "./drizzle",
schema: "./src/lib/server/schema.ts",
dialect: "postgresql",
dbCredentials: {
// biome-ignore lint/style/noNonNullAssertion: We're inside a config, crashing is fine
url: process.env.DATABASE_URL!,
},
casing: "snake_case",
});

View File

@ -0,0 +1,15 @@
CREATE TABLE "sessions" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "sessions_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"user_id" integer,
"expires_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"username" varchar(128) NOT NULL,
"password_hash" text,
"display_name" text,
CONSTRAINT "users_username_unique" UNIQUE("username")
);
--> statement-breakpoint
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;

View File

@ -0,0 +1,131 @@
{
"id": "f22311a8-ce92-4bd5-9a7c-5e8085af13a9",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.sessions": {
"name": "sessions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "sessions_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"expires_at": {
"name": "expires_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "users_id_seq",
"schema": "public",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"username": {
"name": "username",
"type": "varchar(128)",
"primaryKey": false,
"notNull": true
},
"password_hash": {
"name": "password_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"display_name": {
"name": "display_name",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_username_unique": {
"name": "users_username_unique",
"nullsNotDistinct": false,
"columns": [
"username"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1738177092891,
"tag": "0000_users-and-sessions",
"breakpoints": true
}
]
}

8
index.js Normal file
View File

@ -0,0 +1,8 @@
import { loadEnv } from 'vite';
console.log(process.env);
const env = loadEnv("development", ".", "");
// console.log(env);
// console.log(process.env)

View File

@ -21,16 +21,23 @@
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.0.0",
"vitest": "^3.0.0"
"@sveltejs/kit": "^2.16.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/pg": "^8.11.11",
"dotenv": "^16.4.7",
"dotenv-expand": "^12.0.1",
"drizzle-kit": "^0.30.3",
"svelte": "^5.19.5",
"svelte-check": "^4.1.4",
"tsx": "^4.19.2",
"typescript": "^5.7.3",
"vite": "^6.0.11",
"vitest": "^3.0.4"
},
"dependencies": {
"@devcontainers/cli": "^0.72.0",
"postgres": "^3.4.5"
"@devcontainers/cli": "^0.73.0",
"@node-rs/argon2": "^2.0.2",
"drizzle-orm": "^0.39.0",
"pg": "^8.13.1"
}
}

1651
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

11
src/app.d.ts vendored
View File

@ -1,13 +1,18 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
import type { Session } from "$lib/server/session";
import type { User } from "$lib/server/user";
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
user: User | null;
session: Session | null;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@ -1,3 +1,10 @@
import postgres from "postgres";
import { DATABASE_PASSWORD } from "$env/static/private";
import { drizzle } from "drizzle-orm/node-postgres";
const sql = postgres({});
import * as schema from "./schema";
export const db = drizzle({
connection: `postgres://postgres:${DATABASE_PASSWORD}@database/kredens`,
casing: "snake_case",
schema,
});

View File

@ -0,0 +1,12 @@
import { hash, verify } from "@node-rs/argon2";
export async function hashPassword(password: string): Promise<string> {
return await hash(password);
}
export async function verifyPasswordHash(
hash: string,
password: string,
): Promise<boolean> {
return await verify(hash, password);
}

View File

@ -0,0 +1,155 @@
// Source: https://github.com/lucia-auth/example-sveltekit-email-password-2fa/blob/7f9f22d48fb058e4085a11c52528e781c4bc70df/src/lib/server/rate-limit.ts
export class RefillingTokenBucket<_Key> {
public max: number;
public refillIntervalSeconds: number;
constructor(max: number, refillIntervalSeconds: number) {
this.max = max;
this.refillIntervalSeconds = refillIntervalSeconds;
}
private storage = new Map<_Key, RefillBucket>();
public check(key: _Key, cost: number): boolean {
const bucket = this.storage.get(key) ?? null;
if (bucket === null) {
return true;
}
const now = Date.now();
const refill = Math.floor(
(now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000),
);
if (refill > 0) {
return Math.min(bucket.count + refill, this.max) >= cost;
}
return bucket.count >= cost;
}
public consume(key: _Key, cost: number): boolean {
let bucket = this.storage.get(key) ?? null;
const now = Date.now();
if (bucket === null) {
bucket = {
count: this.max - cost,
refilledAt: now,
};
this.storage.set(key, bucket);
return true;
}
const refill = Math.floor(
(now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000),
);
bucket.count = Math.min(bucket.count + refill, this.max);
bucket.refilledAt = now;
if (bucket.count < cost) {
return false;
}
bucket.count -= cost;
this.storage.set(key, bucket);
return true;
}
}
export class Throttler<_Key> {
public timeoutSeconds: number[];
private storage = new Map<_Key, ThrottlingCounter>();
constructor(timeoutSeconds: number[]) {
this.timeoutSeconds = timeoutSeconds;
}
public consume(key: _Key): boolean {
let counter = this.storage.get(key) ?? null;
const now = Date.now();
if (counter === null) {
counter = {
timeout: 0,
updatedAt: now,
};
this.storage.set(key, counter);
return true;
}
const allowed =
now - counter.updatedAt >= this.timeoutSeconds[counter.timeout] * 1000;
if (!allowed) {
return false;
}
counter.updatedAt = now;
counter.timeout = Math.min(
counter.timeout + 1,
this.timeoutSeconds.length - 1,
);
this.storage.set(key, counter);
return true;
}
public reset(key: _Key): void {
this.storage.delete(key);
}
}
export class ExpiringTokenBucket<_Key> {
public max: number;
public expiresInSeconds: number;
private storage = new Map<_Key, ExpiringBucket>();
constructor(max: number, expiresInSeconds: number) {
this.max = max;
this.expiresInSeconds = expiresInSeconds;
}
public check(key: _Key, cost: number): boolean {
const bucket = this.storage.get(key) ?? null;
const now = Date.now();
if (bucket === null) {
return true;
}
if (now - bucket.createdAt >= this.expiresInSeconds * 1000) {
return true;
}
return bucket.count >= cost;
}
public consume(key: _Key, cost: number): boolean {
let bucket = this.storage.get(key) ?? null;
const now = Date.now();
if (bucket === null) {
bucket = {
count: this.max - cost,
createdAt: now,
};
this.storage.set(key, bucket);
return true;
}
if (now - bucket.createdAt >= this.expiresInSeconds * 1000) {
bucket.count = this.max;
}
if (bucket.count < cost) {
return false;
}
bucket.count -= cost;
this.storage.set(key, bucket);
return true;
}
public reset(key: _Key): void {
this.storage.delete(key);
}
}
interface RefillBucket {
count: number;
refilledAt: number;
}
interface ExpiringBucket {
count: number;
createdAt: number;
}
interface ThrottlingCounter {
timeout: number;
updatedAt: number;
}

15
src/lib/server/schema.ts Normal file
View File

@ -0,0 +1,15 @@
import { integer, pgTable, varchar, text, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
username: varchar({length: 128, }).unique().notNull(),
password_hash: text(),
displayName: text(),
});
export const sessions = pgTable("sessions", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userId: integer().references(() => users.id),
expiresAt: timestamp({withTimezone: true}).notNull(),
});

View File

@ -0,0 +1,5 @@
export interface Session {
id: string;
expiresAt: Date;
userId: number;
}

5
src/lib/server/user.ts Normal file
View File

@ -0,0 +1,5 @@
export interface User {
id: number;
username: string;
displayName: string;
}

View File

@ -0,0 +1,12 @@
import { count } from "drizzle-orm";
import { db } from "$lib/server/db";
import { users } from "$lib/server/schema";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async () => {
return {
c: (await db.select({ value: count() }).from(users))[0]
};
};

View File

@ -1,2 +1,10 @@
<script lang="ts">
import type { PageProps } from './$types';
const { data }: PageProps = $props();
</script>
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<p>There is { data.c.value } users!</p>

View File

@ -0,0 +1,18 @@
import { redirect } from "@sveltejs/kit";
import { RefillingTokenBucket, Throttler } from "$lib/server/rate-limit";
import type { Actions, PageServerLoad, PageServerLoadEvent } from "./$types";
const throttler = new Throttler<number>([0, 1, 2, 4, 8, 16, 30, 60, 180, 300]);
const ipBucket = new RefillingTokenBucket<string>(20, 1);
export const load: PageServerLoad = (event: PageServerLoadEvent) => {
if (event.locals.session !== null && event.locals.user !== null) {
return redirect(302, "/");
}
return {};
};
export const actions = {
default: async (event) => {},
} satisfies Actions;

View File