From d6a5445ab5df69ba023a9d5504bbfd870248c977 Mon Sep 17 00:00:00 2001 From: Linus Miller Date: Fri, 19 Dec 2025 08:24:41 +0100 Subject: [PATCH] WIP auth and knex > kysely --- .bruno/API/api-users.bru | 2 +- .bruno/Auth/auth-register.bru | 34 ++++ docker-compose.base.yml | 5 +- ...01-auth_schema.sql => 001_auth_schema.sql} | 180 +++++++----------- docker/postgres/002_error_schema.sql | 86 +++++++++ ...g_schema.sql => 003_accounting_schema.sql} | 81 +++----- .../{03-auth_data.sql => 101_auth_data.sql} | 0 ...nting_data.sql => 103_accounting_data.sql} | 0 docker/postgres/dump.py | 78 ++++++++ docker/postgres/dump.sh | 29 --- server/config/site.ts | 47 +---- server/plugins/auth.ts | 29 +-- server/plugins/auth/routes/register.ts | 149 +++++++++------ server/routes/api.ts | 2 + server/routes/api/admissions.ts | 4 +- server/routes/api/users.ts | 64 +++---- server/schemas/db.ts | 16 ++ server/services/admissions/queries.ts | 95 --------- server/services/admissions/types.ts | 13 -- server/services/invites/queries.ts | 92 --------- server/services/invites/types.ts | 20 -- server/services/roles/queries.ts | 40 ---- server/services/roles/types.ts | 8 - server/services/users/queries.ts | 127 ------------ server/services/users/types.ts | 20 -- 25 files changed, 455 insertions(+), 766 deletions(-) create mode 100644 .bruno/Auth/auth-register.bru rename docker/postgres/{01-auth_schema.sql => 001_auth_schema.sql} (77%) create mode 100644 docker/postgres/002_error_schema.sql rename docker/postgres/{02-accounting_schema.sql => 003_accounting_schema.sql} (95%) rename docker/postgres/{03-auth_data.sql => 101_auth_data.sql} (100%) rename docker/postgres/{04-accounting_data.sql => 103_accounting_data.sql} (100%) create mode 100755 docker/postgres/dump.py delete mode 100755 docker/postgres/dump.sh delete mode 100644 server/services/admissions/queries.ts delete mode 100644 server/services/admissions/types.ts delete mode 100644 server/services/invites/queries.ts delete mode 100644 server/services/invites/types.ts delete mode 100644 server/services/roles/queries.ts delete mode 100644 server/services/roles/types.ts delete mode 100644 server/services/users/queries.ts delete mode 100644 server/services/users/types.ts diff --git a/.bruno/API/api-users.bru b/.bruno/API/api-users.bru index 28284ea..587d727 100644 --- a/.bruno/API/api-users.bru +++ b/.bruno/API/api-users.bru @@ -5,7 +5,7 @@ meta { } get { - url: http://localhost:4040/api/users + url: {{base_url}}/api/users body: none auth: none } diff --git a/.bruno/Auth/auth-register.bru b/.bruno/Auth/auth-register.bru new file mode 100644 index 0000000..095867a --- /dev/null +++ b/.bruno/Auth/auth-register.bru @@ -0,0 +1,34 @@ +meta { + name: /auth/register + type: http + seq: 3 +} + +post { + url: {{base_url}}/auth/register + body: json + auth: inherit +} + +body:json { + { + "email": "linus.k.miller@gmail.com", + "password": "rasmus", + "inviteEmail": "linus.k.miller@gmail.com", + "inviteToken": "1502f035584e09870aab05611161a636f88fb08ccba745850a0430f2bb5b3d8c" + } +} + +body:form-urlencoded { + email: linus.k.miller@gmail.com + password: rasmus +} + +body:multipart-form { + linus.k.miller@gmail.com: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/docker-compose.base.yml b/docker-compose.base.yml index e394321..35feab6 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -25,10 +25,7 @@ services: - POSTGRES_USER=brf_books - POSTGRES_PASSWORD=brf_books volumes: - - ./docker/postgres/01-auth_schema.sql:/docker-entrypoint-initdb.d/01-auth_schema.sql - - ./docker/postgres/02-accounting_schema.sql:/docker-entrypoint-initdb.d/02-accounting_schema.sql - - ./docker/postgres/03-auth_data.sql:/docker-entrypoint-initdb.d/03-auth_data.sql - - ./docker/postgres/04-accounting_data.sql:/docker-entrypoint-initdb.d/04-accounting_data.sql + - ./docker/postgres:/docker-entrypoint-initdb.d - postgres:/var/lib/postgresql/data redis: diff --git a/docker/postgres/01-auth_schema.sql b/docker/postgres/001_auth_schema.sql similarity index 77% rename from docker/postgres/01-auth_schema.sql rename to docker/postgres/001_auth_schema.sql index d45f326..c42a61d 100644 --- a/docker/postgres/01-auth_schema.sql +++ b/docker/postgres/001_auth_schema.sql @@ -2,12 +2,15 @@ -- PostgreSQL database dump -- --- Dumped from database version 16.0 --- Dumped by pg_dump version 16.0 +\restrict kmfsZ1NUIbedynsFb23ZupLqit5AgAIEj3QsIeG1L5YkBtJbYtar24uoNvU1ZrF + +-- Dumped from database version 18.1 +-- Dumped by pg_dump version 18.1 SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; SET client_encoding = 'UTF8'; SET standard_conforming_strings = on; SELECT pg_catalog.set_config('search_path', '', false); @@ -34,6 +37,25 @@ CREATE TABLE public.admission ( ); +-- +-- Name: admissions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.admissions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: admissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.admissions_id_seq OWNED BY public.admission.id; + + -- -- Name: admissions_roles; Type: TABLE; Schema: public; Owner: - -- @@ -45,7 +67,7 @@ CREATE TABLE public.admissions_roles ( -- --- Name: "emailToken"; Type: TABLE; Schema: public; Owner: - +-- Name: emailToken; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public."emailToken" ( @@ -60,7 +82,7 @@ CREATE TABLE public."emailToken" ( -- --- Name: "emailToken_id_seq"; Type: SEQUENCE; Schema: public; Owner: - +-- Name: emailToken_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- CREATE SEQUENCE public."emailToken_id_seq" @@ -72,52 +94,12 @@ CREATE SEQUENCE public."emailToken_id_seq" -- --- Name: "emailToken_id_seq"; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: emailToken_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public."emailToken_id_seq" OWNED BY public."emailToken".id; --- --- Name: error; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.error ( - id integer NOT NULL, - "statusCode" integer, - type text, - message text, - details json, - stack text, - method text, - path text, - headers json, - ip text, - "reqId" text, - "createdAt" timestamp with time zone -); - - --- --- Name: error_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.error_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: error_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.error_id_seq OWNED BY public.error.id; - - -- -- Name: invite; Type: TABLE; Schema: public; Owner: - -- @@ -165,7 +147,7 @@ CREATE TABLE public.invites_roles ( -- --- Name: "passwordToken"; Type: TABLE; Schema: public; Owner: - +-- Name: passwordToken; Type: TABLE; Schema: public; Owner: - -- CREATE TABLE public."passwordToken" ( @@ -179,7 +161,7 @@ CREATE TABLE public."passwordToken" ( -- --- Name: "passwordToken_id_seq"; Type: SEQUENCE; Schema: public; Owner: - +-- Name: passwordToken_id_seq; Type: SEQUENCE; Schema: public; Owner: - -- CREATE SEQUENCE public."passwordToken_id_seq" @@ -191,31 +173,12 @@ CREATE SEQUENCE public."passwordToken_id_seq" -- --- Name: "passwordToken_id_seq"; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- Name: passwordToken_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - -- ALTER SEQUENCE public."passwordToken_id_seq" OWNED BY public."passwordToken".id; --- --- Name: admissions_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.admissions_id_seq - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: admissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.admissions_id_seq OWNED BY public.admission.id; - - -- -- Name: role; Type: TABLE; Schema: public; Owner: - -- @@ -257,16 +220,16 @@ CREATE TABLE public."user" ( id integer NOT NULL, email character varying(254) NOT NULL, password character varying(256) NOT NULL, - "createdAt" timestamp with time zone DEFAULT now() NOT NULL, "lastLoginAt" timestamp with time zone, "loginAttempts" integer DEFAULT 0, "lastLoginAttemptAt" timestamp with time zone, "lastActivityAt" timestamp with time zone, + "emailVerifiedAt" timestamp with time zone, "bannedAt" timestamp with time zone, "bannedById" integer, "blockedAt" timestamp with time zone, "blockedById" integer, - "emailVerifiedAt" timestamp with time zone + "createdAt" timestamp with time zone DEFAULT now() NOT NULL ); @@ -307,19 +270,12 @@ ALTER TABLE ONLY public.admission ALTER COLUMN id SET DEFAULT nextval('public.ad -- --- Name: "emailToken" id; Type: DEFAULT; Schema: public; Owner: - +-- Name: emailToken id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public."emailToken" ALTER COLUMN id SET DEFAULT nextval('public."emailToken_id_seq"'::regclass); --- --- Name: error id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.error ALTER COLUMN id SET DEFAULT nextval('public.error_id_seq'::regclass); - - -- -- Name: invite id; Type: DEFAULT; Schema: public; Owner: - -- @@ -328,7 +284,7 @@ ALTER TABLE ONLY public.invite ALTER COLUMN id SET DEFAULT nextval('public.invit -- --- Name: "passwordToken" id; Type: DEFAULT; Schema: public; Owner: - +-- Name: passwordToken id; Type: DEFAULT; Schema: public; Owner: - -- ALTER TABLE ONLY public."passwordToken" ALTER COLUMN id SET DEFAULT nextval('public."passwordToken_id_seq"'::regclass); @@ -356,6 +312,14 @@ ALTER TABLE ONLY public.admission ADD CONSTRAINT admission_pkey PRIMARY KEY (id); +-- +-- Name: admission admission_regex_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.admission + ADD CONSTRAINT admission_regex_key UNIQUE (regex); + + -- -- Name: admissions_roles admissions_roles_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -365,7 +329,7 @@ ALTER TABLE ONLY public.admissions_roles -- --- Name: "emailToken" email_token_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: emailToken email_token_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."emailToken" @@ -373,21 +337,13 @@ ALTER TABLE ONLY public."emailToken" -- --- Name: "emailToken" email_token_unique; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: emailToken email_token_unique; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."emailToken" ADD CONSTRAINT email_token_unique UNIQUE ("userId", email); --- --- Name: error error_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.error - ADD CONSTRAINT error_pkey PRIMARY KEY (id); - - -- -- Name: invite invite_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -405,7 +361,7 @@ ALTER TABLE ONLY public.invites_roles -- --- Name: "passwordToken" "passwordToken_pkey"; Type: CONSTRAINT; Schema: public; Owner: - +-- Name: passwordToken passwordToken_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."passwordToken" @@ -453,63 +409,63 @@ ALTER TABLE ONLY public.users_roles -- --- Name: "fki_admission_createdById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_admission_createdById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_admission_createdById_fkey" ON public.admission USING btree ("createdById"); -- --- Name: "fki_admission_modifiedById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_admission_modifiedById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_admission_modifiedById_fkey" ON public.admission USING btree ("modifiedById"); -- --- Name: "fki_invite_modifiedById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_invite_modifiedById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_invite_modifiedById_fkey" ON public.invite USING btree ("modifiedById"); -- --- Name: "fki_role_createdById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_role_createdById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_role_createdById_fkey" ON public.role USING btree ("createdById"); -- --- Name: "fki_role_modifiedById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_role_modifiedById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_role_modifiedById_fkey" ON public.role USING btree ("modifiedById"); -- --- Name: "fki_user_bannedById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_user_bannedById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_user_bannedById_fkey" ON public."user" USING btree ("bannedById"); -- --- Name: "fki_user_blockedById_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_user_blockedById_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_user_blockedById_fkey" ON public."user" USING btree ("blockedById"); -- --- Name: "fki_users_roles_roleId_fkey"; Type: INDEX; Schema: public; Owner: - +-- Name: fki_users_roles_roleId_fkey; Type: INDEX; Schema: public; Owner: - -- CREATE INDEX "fki_users_roles_roleId_fkey" ON public.users_roles USING btree ("roleId"); -- --- Name: admission "admission_createdById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: admission admission_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.admission @@ -517,7 +473,7 @@ ALTER TABLE ONLY public.admission -- --- Name: admission "admission_modifiedById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: admission admission_modifiedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.admission @@ -525,7 +481,7 @@ ALTER TABLE ONLY public.admission -- --- Name: admissions_roles "admissions_roles_admissionId_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: admissions_roles admissions_roles_admissionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.admissions_roles @@ -533,7 +489,7 @@ ALTER TABLE ONLY public.admissions_roles -- --- Name: admissions_roles "admissions_roles_roleId_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: admissions_roles admissions_roles_roleId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.admissions_roles @@ -541,7 +497,7 @@ ALTER TABLE ONLY public.admissions_roles -- --- Name: "emailToken" "emailToken_userId_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: emailToken emailToken_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."emailToken" @@ -549,7 +505,7 @@ ALTER TABLE ONLY public."emailToken" -- --- Name: invite "invite_consumedById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: invite invite_consumedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.invite @@ -557,7 +513,7 @@ ALTER TABLE ONLY public.invite -- --- Name: invite "invite_createdById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: invite invite_createdById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.invite @@ -565,7 +521,7 @@ ALTER TABLE ONLY public.invite -- --- Name: invite "invite_modifiedById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: invite invite_modifiedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.invite @@ -573,7 +529,7 @@ ALTER TABLE ONLY public.invite -- --- Name: invites_roles "invites_roles_inviteId_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: invites_roles invites_roles_inviteId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.invites_roles @@ -605,7 +561,7 @@ ALTER TABLE ONLY public.role -- --- Name: role "role_modifiedById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: role role_modifiedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.role @@ -613,7 +569,7 @@ ALTER TABLE ONLY public.role -- --- Name: user "user_bannedById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: user user_bannedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."user" @@ -621,7 +577,7 @@ ALTER TABLE ONLY public."user" -- --- Name: user "user_blockedById_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: user user_blockedById_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."user" @@ -629,7 +585,7 @@ ALTER TABLE ONLY public."user" -- --- Name: users_roles "users_roles_roleId_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: users_roles users_roles_roleId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.users_roles @@ -637,7 +593,7 @@ ALTER TABLE ONLY public.users_roles -- --- Name: users_roles "users_roles_userId_fkey"; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: users_roles users_roles_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public.users_roles @@ -648,3 +604,5 @@ ALTER TABLE ONLY public.users_roles -- PostgreSQL database dump complete -- +\unrestrict kmfsZ1NUIbedynsFb23ZupLqit5AgAIEj3QsIeG1L5YkBtJbYtar24uoNvU1ZrF + diff --git a/docker/postgres/002_error_schema.sql b/docker/postgres/002_error_schema.sql new file mode 100644 index 0000000..87b8165 --- /dev/null +++ b/docker/postgres/002_error_schema.sql @@ -0,0 +1,86 @@ +-- +-- PostgreSQL database dump +-- + +\restrict L31sa9yPB4GMmh0f4VYcX32P22LWelqHYoqB6dSuAh5ONY1bK2J71n300uCZbI9 + +-- Dumped from database version 18.1 +-- Dumped by pg_dump version 18.1 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET transaction_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: error; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.error ( + id integer NOT NULL, + "statusCode" integer, + type text, + message text, + details json, + stack text, + method text, + path text, + headers json, + ip text, + "reqId" text, + "createdAt" timestamp with time zone +); + + +-- +-- Name: error_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.error_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: error_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.error_id_seq OWNED BY public.error.id; + + +-- +-- Name: error id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.error ALTER COLUMN id SET DEFAULT nextval('public.error_id_seq'::regclass); + + +-- +-- Name: error error_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.error + ADD CONSTRAINT error_pkey PRIMARY KEY (id); + + +-- +-- PostgreSQL database dump complete +-- + +\unrestrict L31sa9yPB4GMmh0f4VYcX32P22LWelqHYoqB6dSuAh5ONY1bK2J71n300uCZbI9 + diff --git a/docker/postgres/02-accounting_schema.sql b/docker/postgres/003_accounting_schema.sql similarity index 95% rename from docker/postgres/02-accounting_schema.sql rename to docker/postgres/003_accounting_schema.sql index 28e30b4..e168781 100644 --- a/docker/postgres/02-accounting_schema.sql +++ b/docker/postgres/003_accounting_schema.sql @@ -2,7 +2,7 @@ -- PostgreSQL database dump -- -\restrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd +\restrict kNYhdwOhwE9I3bgAzdljyYgB5xyEpjhiaSCeYZfp84v3ey1GpvsdxX4U8Y8fQM3 -- Dumped from database version 18.1 -- Dumped by pg_dump version 18.1 @@ -19,25 +19,6 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; --- --- Name: truncate_tables(character varying); Type: FUNCTION; Schema: public; Owner: - --- - -CREATE FUNCTION public.truncate_tables(username character varying) RETURNS void - LANGUAGE plpgsql - AS $$ -DECLARE - statements CURSOR FOR - SELECT tablename FROM pg_tables - WHERE tableowner = username AND schemaname = 'public'; -BEGIN - FOR stmt IN statements LOOP - EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;'; - END LOOP; -END; -$$; - - SET default_tablespace = ''; SET default_table_access_method = heap; @@ -372,26 +353,6 @@ CREATE TABLE public.supplier ( ); --- --- Name: supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.supplier_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.supplier_id_seq OWNED BY public.supplier.id; - - -- -- Name: supplierType; Type: TABLE; Schema: public; Owner: - -- @@ -422,6 +383,26 @@ CREATE SEQUENCE public."supplierType_id_seq" ALTER SEQUENCE public."supplierType_id_seq" OWNED BY public."supplierType".id; +-- +-- Name: supplier_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.supplier_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: supplier_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.supplier_id_seq OWNED BY public.supplier.id; + + -- -- Name: transaction; Type: TABLE; Schema: public; Owner: - -- @@ -682,6 +663,14 @@ ALTER TABLE ONLY public.object ADD CONSTRAINT object_pkey PRIMARY KEY (id); +-- +-- Name: supplierType supplierType_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public."supplierType" + ADD CONSTRAINT "supplierType_pkey" PRIMARY KEY (id); + + -- -- Name: supplier supplier_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -698,14 +687,6 @@ ALTER TABLE ONLY public.supplier ADD CONSTRAINT "supplier_taxId_key" UNIQUE ("taxId"); --- --- Name: supplierType supplierType_pkey; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public."supplierType" - ADD CONSTRAINT "supplierType_pkey" PRIMARY KEY (id); - - -- -- Name: transaction transaction_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -811,7 +792,7 @@ ALTER TABLE ONLY public."transactionsToObjects" -- --- Name: "transactionsToObjects" transactionsToObjects_transactionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- Name: transactionsToObjects transactionsToObjects_transactionId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- ALTER TABLE ONLY public."transactionsToObjects" @@ -822,5 +803,5 @@ ALTER TABLE ONLY public."transactionsToObjects" -- PostgreSQL database dump complete -- -\unrestrict FugYqvehfvYcZV6n0VXYfKK3pEfWehcjXHsTSddhC5Qcn0530oCENplg6a2CdZd +\unrestrict kNYhdwOhwE9I3bgAzdljyYgB5xyEpjhiaSCeYZfp84v3ey1GpvsdxX4U8Y8fQM3 diff --git a/docker/postgres/03-auth_data.sql b/docker/postgres/101_auth_data.sql similarity index 100% rename from docker/postgres/03-auth_data.sql rename to docker/postgres/101_auth_data.sql diff --git a/docker/postgres/04-accounting_data.sql b/docker/postgres/103_accounting_data.sql similarity index 100% rename from docker/postgres/04-accounting_data.sql rename to docker/postgres/103_accounting_data.sql diff --git a/docker/postgres/dump.py b/docker/postgres/dump.py new file mode 100755 index 0000000..2a1b8b6 --- /dev/null +++ b/docker/postgres/dump.py @@ -0,0 +1,78 @@ +#!/usr/bin/python3 + +import os +import argparse +from subprocess import run +from datetime import datetime + +auth_tables = [ + 'admission', + 'admissions_roles', + 'emailToken', + 'invite', + 'invites_roles', + 'passwordToken', + 'role', + 'user', + 'users_roles', +] + +accounting_tables = [ + 'account', + 'accountBalance', + 'aliasesToSupplier', + 'dimension', + 'entry', + 'file', + 'filesToInvoice', + 'financialYear', + 'invoice', + 'journal', + 'object', + 'supplier', + 'supplierType', + 'transaction', + 'transactionsToObjects', +] + +parser = argparse.ArgumentParser() + +parser.add_argument('-D', '--dir', help='The local location', default='./docker/postgres') +parser.add_argument('-s', '--schema', help='Dump schema') +parser.add_argument('-d', '--data', help='Dump') +parser.add_argument('--auth', action='store_true') +parser.add_argument('--accounting', action='store_true') +parser.add_argument('tables', type=str, nargs='*', help='The tables to dump') +args = parser.parse_args() + +command = ['docker-compose', 'exec', '-T', 'postgres', 'pg_dump', '-U', 'brf_books', '-d', 'brf_books', '-O'] + +for enabled, tables in [ + (args.tables, args.tables), + (args.auth, auth_tables), + (args.accounting, accounting_tables), +]: + if enabled: + for table in tables: + command.extend(['-t', f'"{table}"']) + +if args.schema: + print('dumping schema...') + + with open(os.path.join(args.dir, args.schema), 'w') as f: + run( + [ *command, '-s'], + stdout=f, + stderr=None + ) + print(' done!') + +if args.data: + print('dumping data...') + with open(os.path.join(args.dir, args.data), 'w') as f: + run( + [ *command, '-a'], + stdout=f, + stderr=None + ) + print(' done!') diff --git a/docker/postgres/dump.sh b/docker/postgres/dump.sh deleted file mode 100755 index b48fd41..0000000 --- a/docker/postgres/dump.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/sh - -script_dir=$(dirname $(readlink -f "$0")) - -SCHEMA=true -DATA=true - -while getopts "as" opt; do - case $opt in - "a") - SCHEMA=false - ;; - "s") - DATA=false - ;; - esac -done - -if [ $SCHEMA = "true" ]; then - echo -n "dumping schema..." - docker-compose exec -T postgres pg_dump -U brf_books -d brf_books -s -O > $script_dir/01-schema.sql - echo " done!" -fi - -if [ $DATA = "true" ]; then - echo -n "dumping data..." - docker-compose exec -T postgres pg_dump -U brf_books -d brf_books -a -O > $script_dir/02-data.sql - echo " done!" -fi diff --git a/server/config/site.ts b/server/config/site.ts index a7e1376..60b167c 100644 --- a/server/config/site.ts +++ b/server/config/site.ts @@ -1,29 +1,13 @@ -import _ from 'lodash' import env from '../env.ts' -const domain = 'brf.lkm.nu' - -type SiteConfig = { - title: string - name: string - port: string | null - hostname: string | null - domain: string - protocol: string - localHost: string - host: string | null - url: string - emails: Record -} - -const defaults: SiteConfig = { +export default { title: 'BRF', name: 'brf', - port: null, - hostname: null, - domain, - protocol: 'http', - localHost: `http://localhost:${env.PORT}`, + port: env.PORT, + hostname: env.HOSTNAME, + domain: env.DOMAIN, + protocol: env.PROTOCOL, + localHost: `http://localhost:${env.FASTIFY_PORT}`, get host() { return this.port ? `${this.hostname}:${this.port}` : this.hostname }, @@ -35,22 +19,3 @@ const defaults: SiteConfig = { info: 'contact@bitmill.io', }, } - -export default _.merge( - defaults, - { - development: { - hostname: 'localhost', - port: env.PORT, - }, - - production: { - hostname: domain, - protocol: 'https', - emails: { - robot: `no-reply@${domain}`, - info: `info@${domain}`, - }, - }, - }[env.NODE_ENV as 'development' | 'production'], -) as SiteConfig diff --git a/server/plugins/auth.ts b/server/plugins/auth.ts index 958cc1d..0eeb525 100644 --- a/server/plugins/auth.ts +++ b/server/plugins/auth.ts @@ -1,18 +1,15 @@ import type { FastifyPluginCallback, FastifyRequest } from 'fastify' import fp from 'fastify-plugin' -import UserQueries from '../services/users/queries.ts' +import { jsonArrayFrom } from 'kysely/helpers/postgres' import changePassword from './auth/routes/change_password.ts' import resetPassword from './auth/routes/reset_password.ts' import login from './auth/routes/login.ts' import logout from './auth/routes/logout.ts' import register from './auth/routes/register.ts' import verifyEmail from './auth/routes/verify_email.ts' -import knex from '../lib/knex.ts' -import emitter from '../lib/emitter.ts' import type { User } from '../services/users/types.ts' -const userQueries = UserQueries({ knex, emitter }) const userPromiseSymbol = Symbol('user') const serialize = (user: User) => Promise.resolve(user.id) @@ -20,6 +17,8 @@ const serialize = (user: User) => Promise.resolve(user.id) const auth: FastifyPluginCallback<{ prefix?: string }> = (fastify, options, done) => { const prefix = options.prefix || '' + const { db } = fastify + fastify.decorate('auth', (request, reply, done) => { if (!request.session.userId) return reply.status(401).send() @@ -49,10 +48,20 @@ const auth: FastifyPluginCallback<{ prefix?: string }> = (fastify, options, done fastify.decorateRequest('getUser', function getUser(this: FastifyRequest & { [userPromiseSymbol]?: Promise }) { if (!this.session || !this.session.userId) { return Promise.resolve(null) - } else if (!this[userPromiseSymbol]) { - this[userPromiseSymbol] = userQueries.findById(this.session.userId).catch((err) => { - this.log.error(err) - }) + } + + if (!this[userPromiseSymbol]) { + this[userPromiseSymbol] = db + .selectFrom('user') + .selectAll() + .select((eb) => + jsonArrayFrom( + eb.selectFrom('role as r').innerJoin('users_roles as ur', 'ur.roleId', 'r.id').select(['id', 'name']), + ).as('roles'), + ) + .where('id', '=', this.session.userId) + .executeTakeFirstOrThrow() + .then(({ password: _, ...rest }) => rest) } return this[userPromiseSymbol] @@ -79,10 +88,6 @@ const auth: FastifyPluginCallback<{ prefix?: string }> = (fastify, options, done fastify.get(prefix + '/logout', logout) fastify.post(prefix + '/register', register) - fastify.addHook('onClose', () => { - knex.destroy() - }) - done() } diff --git a/server/plugins/auth/routes/register.ts b/server/plugins/auth/routes/register.ts index 50e733e..3983233 100644 --- a/server/plugins/auth/routes/register.ts +++ b/server/plugins/auth/routes/register.ts @@ -2,21 +2,12 @@ import _ from 'lodash' import * as z from 'zod' import type { RouteHandler } from 'fastify' import config from '../../../config.ts' -import emitter from '../../../lib/emitter.ts' -import knex from '../../../lib/knex.ts' -import AdmissionQueries from '../../../services/admissions/queries.ts' -import InviteQueries from '../../../services/invites/queries.ts' -import UserQueries from '../../../services/users/queries.ts' import verifyEmailTemplate from '../../../templates/emails/verify_email.ts' import sendMail from '../../../lib/send_mail.ts' import StatusError from '../../../lib/status_error.ts' import { generateToken, hashPassword } from '../helpers.ts' -import type { Invite } from '../../../services/invites/types.ts' -import type { Role } from '../../../services/roles/types.ts' - -const admissionQueries = AdmissionQueries({ knex, emitter }) -const inviteQueries = InviteQueries({ knex, emitter }) -const userQueries = UserQueries({ knex, emitter }) +import type { Invite } from '../../../../shared/types.db.ts' +import { sql, type Selectable } from 'kysely' const { errors, timeouts } = config.auth @@ -44,23 +35,50 @@ const ResponseSchema = { '4XX': { $ref: 'status-error' }, } +type InviteMinimal = Pick, 'id' | 'email' | 'createdAt' | 'consumedAt'> & { roleIds?: number[] } + const register: RouteHandler<{ Body: z.infer }> = async function (request, reply) { + const { db } = request.server + try { // TODO validate and ensure same as confirm const email = request.body.email.trim().toLowerCase() - let invite: Invite | null = null + let invite: InviteMinimal | null | undefined = null if (request.body.inviteEmail && request.body.inviteToken) { - invite = await inviteQueries.findOne({ email: request.body.inviteEmail, token: request.body.inviteToken }) - const latestInvite = await inviteQueries.findOne({ - email: request.body.inviteEmail, - sort: [{ column: 'createdAt', order: 'desc' }], - }) + let latestInvite: { id: number } | null | undefined = null + + ;[invite, latestInvite] = await Promise.all([ + db + .selectFrom('invite as i') + .innerJoin('invites_roles as ir', 'ir.inviteId', 'i.id') + .select([ + 'id', + 'email', + 'createdAt', + 'consumedAt', + (eb) => eb.fn.agg('array_agg', ['ir.roleId']).as('roleIds'), + ]) + .where((eb) => + eb.and({ + email: request.body.inviteEmail, + token: request.body.inviteToken, + }), + ) + .groupBy('id') + .executeTakeFirst(), + db + .selectFrom('invite') + .select('id') + .where('email', '=', request.body.inviteEmail) + .orderBy('createdAt', 'desc') + .executeTakeFirst(), + ]) if (!invite) { throw new StatusError(...errors.tokenNotFound) - } else if (invite.id !== latestInvite.id) { + } else if (invite.id !== latestInvite!.id) { throw new StatusError(...errors.tokenNotLatest) } else if (Date.now() > new Date(invite.createdAt).getTime() + timeouts.invite) { throw new StatusError(...errors.tokenExpired) @@ -69,73 +87,84 @@ const register: RouteHandler<{ Body: z.infer }> = async funct } } - const admissions = await admissionQueries.findMatches(email) + const admissions = await db + .selectFrom('admission as a') + .innerJoin('admissions_roles as ar', 'ar.admissionId', 'a.id') + .select(['regex', (eb) => eb.fn.agg('array_agg', ['ar.roleId']).as('roleIds')]) + .groupBy('regex') + .execute() + .then((admissions) => admissions.filter((admission) => new RegExp(admission.regex).test(email))) - const roles = Array.from( - new Set( - [ - invite?.roles ? invite.roles.map((role) => role.id) : [], - ...admissions.map((admission) => (admission.roles ? admission.roles.map((role: Role) => role.id) : [])), - ].flat(), - ), + const roleIds: number[] = Array.from( + new Set([invite?.roleIds || [], ...admissions.map((admission) => admission.roleIds || [])].flat() as number[]), ) - if (!roles.length) { + if (!roleIds.length) { throw new StatusError(...errors.notAuthorized) } - const user = await knex.transaction(async (trx) => { - const user = await userQueries.create( - { - email, - password: await hashPassword(request.body.password), - roles, - emailVerifiedAt: invite && invite.email === email ? (knex.fn.now() as unknown as string) : null, - }, - trx, - ) + const trx = await db.startTransaction().execute() - if (invite) { - await inviteQueries.consume(invite.id, user.id, trx) - } + const user = await trx + .insertInto('user') + .values({ + email, + password: await hashPassword(request.body.password), + emailVerifiedAt: invite?.email === email ? sql`now()` : null, + }) + .returningAll() + .executeTakeFirstOrThrow() - if (!user.emailVerifiedAt) { - const token = generateToken() + await trx + .insertInto('users_roles') + .values(roleIds.map((roleId) => ({ roleId, userId: user.id }))) + .execute() - await trx('email_token').insert({ - user_id: user.id, + if (invite) { + trx + .updateTable('invite') + .set({ consumedAt: sql`now()`, consumedById: user.id }) + .execute() + } + + if (!user.emailVerifiedAt) { + const token = generateToken() + + await trx + .insertInto('emailToken') + .values({ + userId: user.id, email: user.email, token, }) + .execute() - const link = `${new URL('/auth/verify-email', config.site.url)}?email=${user.email}&token=${token}` + const link = `${new URL('/auth/verify-email', config.site.url)}?email=${user.email}&token=${token}` - await sendMail({ - to: user.email, - subject: `Verify ${config.site.title} account`, - html: await verifyEmailTemplate({ link }).text(), - }) - } + await sendMail({ + to: user.email, + subject: `Verify ${config.site.title} account`, + html: await verifyEmailTemplate({ link }).text(), + }) + } - return userQueries.findById(user.id) - }) + await trx.commit().execute() + + const roles = await db.selectFrom('role').select(['id', 'name']).where('id', 'in', roleIds).execute() reply.type('application/json') - return reply.status(201).send(_.omit(user, 'password')) + return reply.status(201).send(Object.assign(_.omit(user, 'password'), { roles })) } catch (err) { this.log.error(err) // @ts-ignore if (err.code == 23505) { - err = new StatusError(...errors.duplicateEmail) - } - - if (err instanceof StatusError) { + const statusError = new StatusError(...errors.duplicateEmail) return reply - .status(err.status || 500) + .status(statusError.status || 500) .type('application/json') - .send(err.toJSON()) + .send(statusError.toJSON()) } else { throw err } diff --git a/server/routes/api.ts b/server/routes/api.ts index 9af3c39..767e29b 100644 --- a/server/routes/api.ts +++ b/server/routes/api.ts @@ -15,6 +15,7 @@ import results from './api/results.ts' import roles from './api/roles.ts' import suppliers from './api/suppliers.ts' import transactions from './api/transactions.ts' +import users from './api/users.ts' const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { fastify.register(accounts, { prefix: '/accounts' }) @@ -31,6 +32,7 @@ const apiRoutes: FastifyPluginCallbackTypebox = (fastify, _, done) => { fastify.register(roles, { prefix: '/roles' }) fastify.register(suppliers, { prefix: '/suppliers' }) fastify.register(transactions, { prefix: '/transactions' }) + fastify.register(users, { prefix: '/users' }) done() } diff --git a/server/routes/api/admissions.ts b/server/routes/api/admissions.ts index 87d8f86..13bd506 100644 --- a/server/routes/api/admissions.ts +++ b/server/routes/api/admissions.ts @@ -8,7 +8,7 @@ import { AdmissionSchema, RoleSchema } from '../../schemas/db.ts' const admissionsPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => { const { db } = fastify - fastify.addHook('onRequest', fastify.auth) + // fastify.addHook('onRequest', fastify.auth) fastify.route({ url: '/', @@ -90,7 +90,7 @@ const admissionsPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => method: 'DELETE', schema: { params: z.object({ - id: z.number(), + id: z.coerce.number(), }), response: { 204: {}, diff --git a/server/routes/api/users.ts b/server/routes/api/users.ts index fde93d1..063e25a 100644 --- a/server/routes/api/users.ts +++ b/server/routes/api/users.ts @@ -1,44 +1,10 @@ +import * as z from 'zod' import type { FastifyPluginCallbackZod } from 'fastify-type-provider-zod' -import knex from '../../lib/knex.ts' -import emitter from '../../lib/emitter.ts' -import Queries from '../../services/users/queries.ts' -const usersPlugin: FastifyPluginCallbackZod<{ addParentSchema: (schema: ANY) => void }> = ( - fastify, - { addParentSchema }, - done, -) => { - const queries = Queries({ emitter, knex }) +import { UserSchema } from '../../schemas/db.ts' - addParentSchema({ - $id: 'user', - type: 'object', - properties: { - id: { type: 'integer' }, - email: { type: 'string' }, - password: { type: 'string' }, - createdAt: { type: 'string' }, - lastLoginAt: { type: 'string' }, - lastLoginAttemptAt: { type: 'string' }, - loginAttempts: { type: 'integer' }, - bannedAt: { type: 'string' }, - bannedById: { type: 'integer' }, - blockedAt: { type: 'string' }, - blockedById: { type: 'integer' }, - emailVerifiedAt: { type: 'string' }, - roles: { type: ['array', 'null'], items: { $ref: 'role' } }, - }, - }) - - addParentSchema({ - $id: 'user-short', - type: 'object', - properties: { - id: { type: 'integer' }, - email: { type: 'string' }, - createdAt: { type: 'string' }, - }, - }) +const usersPlugin: FastifyPluginCallbackZod = (fastify, _options, done) => { + const { db } = fastify fastify.addHook('onRequest', fastify.auth) @@ -47,11 +13,27 @@ const usersPlugin: FastifyPluginCallbackZod<{ addParentSchema: (schema: ANY) => method: 'GET', schema: { response: { - 200: { type: 'array', items: { $ref: 'user' } }, + 200: z.array(UserSchema.omit({ password: true })), }, }, - handler(request) { - return queries.find(request.query) + handler() { + return db + .selectFrom('user') + .select([ + 'id', + 'email', + 'lastLoginAt', + 'loginAttempts', + 'lastLoginAttemptAt', + 'lastActivityAt', + 'bannedAt', + 'bannedById', + 'blockedAt', + 'blockedById', + 'emailVerifiedAt', + 'createdAt', + ]) + .execute() }, }) diff --git a/server/schemas/db.ts b/server/schemas/db.ts index 9efbe5c..baf6075 100644 --- a/server/schemas/db.ts +++ b/server/schemas/db.ts @@ -71,3 +71,19 @@ export const SupplierTypeSchema = z.object({ id: z.number().int().optional(), name: z.string(), }) + +export const UserSchema = z.object({ + id: z.number().int().optional(), + email: z.string(), + password: z.string(), + createdAt: z.date().optional(), + lastLoginAt: z.date().nullable().optional(), + loginAttempts: z.number().int().nullable().optional(), + lastLoginAttemptAt: z.date().nullable().optional(), + lastActivityAt: z.date().nullable().optional(), + bannedAt: z.date().nullable().optional(), + bannedById: z.number().int().nullable().optional(), + blockedAt: z.date().nullable().optional(), + blockedById: z.number().int().nullable().optional(), + emailVerifiedAt: z.date().nullable().optional(), +}) diff --git a/server/services/admissions/queries.ts b/server/services/admissions/queries.ts deleted file mode 100644 index 2619edd..0000000 --- a/server/services/admissions/queries.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type EventEmitter from 'node:events' -import type { Knex } from 'knex' -import _ from 'lodash' - -import RestQueriesFactory from '../../lib/knex_rest_queries.ts' - -import type { NewAdmission } from './types.ts' - -export const columns = ['id', 'regex', 'createdAt', 'createdById', 'modifiedAt', 'modifiedById'] - -export const Selects = (knex: Knex) => ({ - roles: knex - .select(knex.raw('json_agg(roles)')) - .from( - knex - .select('id', 'name') - .from('role') - .innerJoin('admissions_roles', 'role.id', 'admissions_roles.role_id') - .where('admissions_roles.admission_id', knex.ref('admission.id')) - .orderBy('name') - .as('roles'), - ), - - createdBy: knex - .select(knex.raw('row_to_json("user")')) - .from(knex.select('id', 'email').from('user').where('id', knex.ref('admission.created_by_id')).as('user')), -}) - -export default ({ knex, emitter }: { emitter: EventEmitter; knex: Knex }) => { - const selects = Selects(knex) - const queries = RestQueriesFactory({ - knex, - emitter, - table: 'admission', - columns, - selects, - }) - - return { - ...queries, - - create(json: NewAdmission, client = knex) { - return client.transaction(async (trx) => { - const admission = await queries.create(_.omit(json, 'roles'), trx) - - const roles = json.roles.map((role) => ({ - admission_id: admission.id, - role_id: role, - })) - - await trx.table('admissions_roles').insert(roles) - - return queries.findById(admission.id, trx) - }) - }, - - update(id: number, json: NewAdmission, client = knex) { - return client.transaction(async (trx) => { - await queries.update(id, _.omit(json, ['roles']), trx) - - // TODO decide how to handle this, ie should an empty array delete all? - if (json.roles?.length) { - await trx.table('admissions_roles').delete().where('admission_id', id).whereNotIn('role_id', json.roles) - - await trx.raw( - `INSERT INTO admissions_roles(admission_id, role_id) - SELECT :admissionId, role_ids FROM unnest(:roleIds::int[]) AS role_ids WHERE NOT EXISTS - (SELECT 1 FROM admissions_roles WHERE admission_id = :admissionId AND role_id = role_ids)`, - { admissionId: id, roleIds: json.roles }, - ) - } - - return queries.findById(id, trx) - }) - }, - - findMatches(email: string, client = knex) { - return client('admission') - .select([...columns, selects]) - .then((admissions) => { - if (admissions) { - admissions = admissions.filter((admission) => { - const regex = new RegExp(admission.regex) - - return regex.test(email) - }) - - // if (!admissions.length) admissions = undefined - } - - return admissions - }) - }, - } -} diff --git a/server/services/admissions/types.ts b/server/services/admissions/types.ts deleted file mode 100644 index ac5b466..0000000 --- a/server/services/admissions/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type Admission = { - id: number - regex: string - createdAt: string - createdById: string - modifiedAt: string | null - modifiedById: number | null -} - -export type NewAdmission = { - regex: string - roles: number[] -} diff --git a/server/services/invites/queries.ts b/server/services/invites/queries.ts deleted file mode 100644 index aa40233..0000000 --- a/server/services/invites/queries.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type EventEmitter from 'node:events' -import crypto from 'node:crypto' -import type { Knex } from 'knex' -import _ from 'lodash' -import type { NewInvite } from './types.ts' - -import RestQueriesFactory from '../../lib/knex_rest_queries.ts' - -export const columns = [ - 'id', - 'email', - 'token', - 'createdAt', - 'createdById', - 'modifiedAt', - 'modifiedById', - 'consumedAt', - 'consumedById', -] - -export const Selects = (knex: Knex) => ({ - roles: knex - .select(knex.raw('json_agg(roles)')) - .from( - knex - .select('id', 'name') - .from('role') - .innerJoin('invites_roles', 'role.id', 'invites_roles.role_id') - .where('invites_roles.invite_id', knex.ref('invite.id')) - .as('roles'), - ), - - createdBy: knex - .select(knex.raw('row_to_json(users)')) - .from(knex.select('id', 'email').from('user').where('id', knex.ref('invite.created_by_id')).as('users')), - - consumedBy: knex - .select(knex.raw('row_to_json(users)')) - .from(knex.select('id', 'email').from('user').where('id', knex.ref('invite.consumed_by_id')).as('users')), -}) - -function generateToken(length = 64) { - return crypto.randomBytes(length / 2).toString('hex') -} - -export default ({ knex, emitter }: { knex: Knex; emitter: EventEmitter }) => { - const queries = RestQueriesFactory({ - emitter, - knex, - table: 'invite', - columns, - selects: Selects(knex), - }) - - function consume(id: number, consumedById: number, client = knex) { - return client('invite') - .update({ consumed_at: knex.fn.now(), consumed_by_id: consumedById }) - .where('id', id) - .then((result: ANY) => { - if (result === 0) throw new Error('No invite was consumed') - - return !!result - }) - } - - async function create(json: NewInvite, client: Knex | Knex.Transaction = knex) { - const token = generateToken() - - const trx = client.isTransaction ? (client as Knex.Transaction) : await client.transaction() - - const invite = await queries.create({ ..._.omit(json, 'roles'), token }, trx) - - const roles = json.roles.map((role: number) => ({ - invite_id: invite.id, - role_id: role, - })) - - await trx.table('invites_roles').insert(roles) - - if (trx !== client) { - await trx.commit() - } - - return queries.findById(invite.id, trx) - } - - return { - ...queries, - consume, - create, - } -} diff --git a/server/services/invites/types.ts b/server/services/invites/types.ts deleted file mode 100644 index c0c9f94..0000000 --- a/server/services/invites/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Role } from '../roles/types.ts' - -export type Invite = { - id: number - email: string - token: string - createdAt: string - createdById: number - modifiedAt: string | null - modifiedById: number | null - consumedAt: string | null - consumedById: number | null - roles: Role[] -} - -export type NewInvite = { - email: string - createdById: number - roles: number[] -} diff --git a/server/services/roles/queries.ts b/server/services/roles/queries.ts deleted file mode 100644 index 0b41437..0000000 --- a/server/services/roles/queries.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Knex } from 'knex' -import _ from 'lodash' - -import RestQueriesFactory from '../../lib/knex_rest_queries.ts' -import emitter from '../../lib/emitter.ts' - -export const columns = ['id', 'name', 'createdAt', 'createdById', 'modifiedById', 'modifiedAt'] - -export const Selects = (knex: Knex) => ({ - createdBy: knex - .select(knex.raw('row_to_json(users)')) - .from(knex.select('id', 'email', 'createdAt').from('user').where('id', knex.ref('role.created_by_id')).as('users')), - modifiedBy: knex - .select(knex.raw('row_to_json(users)')) - .from( - knex.select('id', 'email', 'createdAt').from('user').where('id', knex.ref('role.modified_by_id')).as('users'), - ), -}) - -export default ({ knex }: { knex: Knex }) => { - function findByIds(ids: string[], client = knex) { - return client.table('role').select(columns).whereIn('id', ids).then(_.identity) - } - - function findByNames(names: string[], client = knex) { - return client.table('role').select(columns).whereIn('name', names).then(_.identity) - } - - return { - ...RestQueriesFactory({ - emitter, - knex, - table: 'role', - columns, - selects: Selects(knex), - }), - findByIds, - findByNames, - } -} diff --git a/server/services/roles/types.ts b/server/services/roles/types.ts deleted file mode 100644 index 03b4a63..0000000 --- a/server/services/roles/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Role = { - id: number - name: string - createdAt: string - createdById: number - modifedAt: string | null - modifiedById: string | null -} diff --git a/server/services/users/queries.ts b/server/services/users/queries.ts deleted file mode 100644 index fdc478a..0000000 --- a/server/services/users/queries.ts +++ /dev/null @@ -1,127 +0,0 @@ -import EventEmitter from 'events' -import type { Knex } from 'knex' -import _ from 'lodash' -import type { User, NewUser } from './types.ts' - -import RestQueriesFactory from '../../lib/knex_rest_queries.ts' - -export const columns = [ - 'bannedAt', - 'bannedById', - 'blockedAt', - 'blockedById', - 'createdAt', - 'email', - 'emailVerifiedAt', - 'id', - 'lastActivityAt', - 'lastLoginAt', - 'lastLoginAttemptAt', - 'loginAttempts', - 'password', -] - -export const Selects = (knex: Knex) => ({ - roles: knex - .select(knex.raw('json_agg(roles)')) - .from( - knex - .select('id', 'name') - .from('role') - .innerJoin('users_roles', 'role.id', 'users_roles.roleId') - .where('users_roles.userId', knex.ref('user.id')) - .as('roles'), - ), -}) - -interface Options { - emitter: EventEmitter - knex: Knex -} - -export default ({ emitter, knex }: Options) => { - const userQueries = RestQueriesFactory({ - knex, - emitter, - omit: ['replace'], - table: 'user', - columns, - selects: Selects(knex), - }) - - async function create(json: NewUser, client: Knex | Knex.Transaction = knex) { - const trx = (client.isTransaction ? client : await client.transaction()) as Knex.Transaction - - const user = await userQueries.create(json, trx) - - if (json.roles) { - await trx('users_roles').insert(json.roles.map((role) => ({ userId: user.id, roleId: role }))) - } - - if (trx !== client) { - await trx.commit() - } - - return userQueries.findById(user.id, trx) - } - - function onActivity(id: number, client = knex) { - return update(id, { lastActivityAt: 'now()' }, client) - } - - function onLogin(id: number, client = knex) { - return update(id, { lastLoginAt: 'now()', loginAttempts: 0, lastLoginAttemptAt: null }, client) - } - - function onLoginAttempt(id: number, client = knex) { - return client('user') - .update({ last_login_attempt_at: knex.fn.now(), login_attempts: knex.raw('login_attempts + 1') }) - .where('id', id) - } - - function replace(id: number, json: Partial, client = knex) { - return update(id, json, client) - } - - function update(id: number, json: Partial, client = knex) { - return client.transaction((trx) => - userQueries - .update(id, _.omit(json, ['roles']), trx) - .then(() => { - const roles = json.roles - - if (roles && roles.length) { - const roleIds = roles.map((role) => role.id) - - return trx.raw( - `WITH deleted_rows AS ( - DELETE FROM users_roles - WHERE "userId" = :userId AND "roleId" NOT IN - (SELECT "roleIds" FROM unnest(:roleIds::int[]) AS "roleIds") - RETURNING "roleId" - ), inserted_rows AS ( - INSERT INTO users_roles("userId", "roleId") - SELECT :userId, "roleIds" FROM unnest(:roleIds::int[]) AS "roleIds" WHERE NOT EXISTS - (SELECT 1 FROM users_roles WHERE user_id = :userId AND "roleId" = "roleIds") - RETURNING "roleId", "userId" - ) - - SELECT "roleId", "userId" FROM inserted_rows;`, - [id, roleIds], - ) - } - }) - .then(() => userQueries.findById(id)), - ) - } - - return { - ...userQueries, - create, - onActivity, - onLogin, - onLoginAttempt, - replace, - update, - } -} diff --git a/server/services/users/types.ts b/server/services/users/types.ts deleted file mode 100644 index d8b4413..0000000 --- a/server/services/users/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Role } from '../roles/types.ts' - -export type User = { - bannedAt: string | null - bannedById: number | null - blockedAt: string | null - blockedById: number | null - createdAt: string - email: string - emailVerifiedAt: string | null - id: number - lastActivityAt: string | null - lastLoginAt: string | null - lastLoginAttemptAt: string | null - loginAttempts: number - password: string - roles: Role[] -} - -export type NewUser = Pick & { roles: number[] }