Just working with a DB and stuff
This commit is contained in:
parent
b8c555d8e7
commit
32281aa1ff
5
.env
Normal file
5
.env
Normal 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
2
.env.development
Normal file
@ -0,0 +1,2 @@
|
||||
DATABASE_USER=postgres
|
||||
DATABASE_HOST=database
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
13
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"sqltools.connections": [
|
||||
{
|
||||
"previewLimit": 50,
|
||||
"server": "database",
|
||||
"port": 5432,
|
||||
"driver": "PostgreSQL",
|
||||
"name": "Dev Container Backend",
|
||||
"database": "kredens",
|
||||
"username": "postgres"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
21
drizzle.config.ts
Normal 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",
|
||||
});
|
15
drizzle/0000_users-and-sessions.sql
Normal file
15
drizzle/0000_users-and-sessions.sql
Normal 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;
|
131
drizzle/meta/0000_snapshot.json
Normal file
131
drizzle/meta/0000_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal 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
8
index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { loadEnv } from 'vite';
|
||||
|
||||
console.log(process.env);
|
||||
|
||||
const env = loadEnv("development", ".", "");
|
||||
|
||||
// console.log(env);
|
||||
// console.log(process.env)
|
25
package.json
25
package.json
@ -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
1651
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
11
src/app.d.ts
vendored
11
src/app.d.ts
vendored
@ -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 {};
|
||||
|
@ -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,
|
||||
});
|
||||
|
12
src/lib/server/password.ts
Normal file
12
src/lib/server/password.ts
Normal 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);
|
||||
}
|
155
src/lib/server/rate-limit.ts
Normal file
155
src/lib/server/rate-limit.ts
Normal 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
15
src/lib/server/schema.ts
Normal 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(),
|
||||
});
|
||||
|
5
src/lib/server/session.ts
Normal file
5
src/lib/server/session.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Session {
|
||||
id: string;
|
||||
expiresAt: Date;
|
||||
userId: number;
|
||||
}
|
5
src/lib/server/user.ts
Normal file
5
src/lib/server/user.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
displayName: string;
|
||||
}
|
12
src/routes/+page.server.ts
Normal file
12
src/routes/+page.server.ts
Normal 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]
|
||||
};
|
||||
};
|
@ -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>
|
18
src/routes/login/+page.server.ts
Normal file
18
src/routes/login/+page.server.ts
Normal 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;
|
0
src/routes/login/+page.svelte
Normal file
0
src/routes/login/+page.svelte
Normal file
Loading…
x
Reference in New Issue
Block a user