Slight restructure, updated auth, implement player and game endpoints
This commit is contained in:
2
.env.dev
2
.env.dev
@@ -1,2 +1,2 @@
|
|||||||
DATABASE_URL=postgres://admin:iiyama12@192.168.1.166:5432/bgApp
|
DATABASE_URL=postgres://ApiUser:2<KtJ=*5`;19@192.168.1.166:5432/bgApp
|
||||||
JWT_SECRET_KEY=MySecret
|
JWT_SECRET_KEY=MySecret
|
||||||
12
.prettierrc.json
Normal file
12
.prettierrc.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 120,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"singleAttributePerLine": true
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
POST http://localhost:3000/api/auth/login
|
|
||||||
Content-Type: application/json
|
|
||||||
|
|
||||||
{
|
|
||||||
"username": "jd",
|
|
||||||
"password": "Foobar"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
GET http://localhost:3000/api/auth/test
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIiwiY2xhaW1zIjpbIkFETUlOIiwiVVNFUlNfQ1JFQVRFIiwiVVNFUlNfT1RIRVJfVVBEQVRFIiwiVVNFUlNfU0VMRl9SRUFEIiwiVVNFUlNfU0VMRl9VUERBVEUiLCJVU0VSU19PVEhFUl9SRUFEIiwiVVNFUlNfU0VMRl9ERUxFVEUiXSwiaWF0IjoxNzcxMDEyNDM5LCJleHAiOjE3NzEwOTg4Mzl9.__EHi3dO_uG1mtCVhmRqVKTkbTkOzM5Hu-4gMrIfu7I
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
POST http://localhost:3000/api/user
|
|
||||||
Content-Type: application/json
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIiwiY2xhaW1zIjpbIlVTRVJTX1NFTEZfUkVBRCIsIkFETUlOIiwiVVNFUlNfQ1JFQVRFIiwiVVNFUlNfT1RIRVJfUkVBRCIsIlVTRVJTX1NFTEZfREVMRVRFIiwiVVNFUlNfT1RIRVJfVVBEQVRFIiwiVVNFUlNfU0VMRl9VUERBVEUiXSwiaWF0IjoxNzcwOTE1NjY4LCJleHAiOjE3NzEwMDIwNjh9.7ZWfIcT9vBIpqYY4PhJspRPrCtyBkqQ5jmSjOrCgzWI
|
|
||||||
|
|
||||||
{
|
|
||||||
"username": "jd8",
|
|
||||||
"password": "Foobar2"
|
|
||||||
}
|
|
||||||
|
|
||||||
###
|
|
||||||
GET http://localhost:3000/api/user/2
|
|
||||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIiwiY2xhaW1zIjpbIkFETUlOIiwiVVNFUlNfQ1JFQVRFIiwiVVNFUlNfT1RIRVJfVVBEQVRFIiwiVVNFUlNfU0VMRl9SRUFEIiwiVVNFUlNfU0VMRl9VUERBVEUiLCJVU0VSU19PVEhFUl9SRUFEIiwiVVNFUlNfU0VMRl9ERUxFVEUiXSwiaWF0IjoxNzcxMDE5NzMzLCJleHAiOjE3NzExMDYxMzN9.V4La9Sv13M15lubtxWGESUksWldhlyG8AhiIE4zAtWo
|
|
||||||
@@ -13,8 +13,10 @@
|
|||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/lodash": "^4.17.23",
|
"@types/lodash": "^4.17.23",
|
||||||
"argon2": "^0.44.0",
|
"argon2": "^0.44.0",
|
||||||
|
"hashids": "^2.3.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lodash": "^4.17.23"
|
"lodash": "^4.17.23",
|
||||||
|
"reflect-metadata": "^0.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.9"
|
"@types/bun": "^1.3.9"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
-- Dumped from database version 18.1 (Debian 18.1-1.pgdg13+2)
|
-- Dumped from database version 18.1 (Debian 18.1-1.pgdg13+2)
|
||||||
-- Dumped by pg_dump version 18.1
|
-- Dumped by pg_dump version 18.1
|
||||||
|
|
||||||
-- Started on 2026-02-13 18:14:42 GMT
|
-- Started on 2026-02-18 20:07:03 GMT
|
||||||
|
|
||||||
SET statement_timeout = 0;
|
SET statement_timeout = 0;
|
||||||
SET lock_timeout = 0;
|
SET lock_timeout = 0;
|
||||||
@@ -19,17 +19,88 @@ SET xmloption = content;
|
|||||||
SET client_min_messages = warning;
|
SET client_min_messages = warning;
|
||||||
SET row_security = off;
|
SET row_security = off;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2 (class 3079 OID 20739)
|
||||||
|
-- Name: pg_trgm; Type: EXTENSION; Schema: -; Owner: -
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3606 (class 0 OID 0)
|
||||||
|
-- Dependencies: 2
|
||||||
|
-- Name: EXTENSION pg_trgm; Type: COMMENT; Schema: -; Owner:
|
||||||
|
--
|
||||||
|
|
||||||
|
COMMENT ON EXTENSION pg_trgm IS 'text similarity measurement and index searching based on trigrams';
|
||||||
|
|
||||||
|
|
||||||
SET default_tablespace = '';
|
SET default_tablespace = '';
|
||||||
|
|
||||||
SET default_table_access_method = heap;
|
SET default_table_access_method = heap;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 224 (class 1259 OID 16431)
|
-- TOC entry 243 (class 1259 OID 19974)
|
||||||
|
-- Name: circle_comments; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.circle_comments (
|
||||||
|
circle_id bigint NOT NULL,
|
||||||
|
comment_id bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.circle_comments OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 230 (class 1259 OID 19837)
|
||||||
|
-- Name: circles; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.circles (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
name character varying(60) NOT NULL,
|
||||||
|
is_public boolean DEFAULT false NOT NULL,
|
||||||
|
owning_user_id bigint NOT NULL,
|
||||||
|
image_path text,
|
||||||
|
colour character(7) DEFAULT '#71677C'::bpchar NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.circles OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 229 (class 1259 OID 19836)
|
||||||
|
-- Name: circles_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.circles_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.circles_id_seq OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3607 (class 0 OID 0)
|
||||||
|
-- Dependencies: 229
|
||||||
|
-- Name: circles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.circles_id_seq OWNED BY public.circles.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 225 (class 1259 OID 16431)
|
||||||
-- Name: claims; Type: TABLE; Schema: public; Owner: admin
|
-- Name: claims; Type: TABLE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public.claims (
|
CREATE TABLE public.claims (
|
||||||
id integer NOT NULL,
|
id bigint NOT NULL,
|
||||||
name character varying(60) NOT NULL,
|
name character varying(60) NOT NULL,
|
||||||
is_default boolean DEFAULT false NOT NULL
|
is_default boolean DEFAULT false NOT NULL
|
||||||
);
|
);
|
||||||
@@ -38,7 +109,7 @@ CREATE TABLE public.claims (
|
|||||||
ALTER TABLE public.claims OWNER TO admin;
|
ALTER TABLE public.claims OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 223 (class 1259 OID 16430)
|
-- TOC entry 224 (class 1259 OID 16430)
|
||||||
-- Name: claims_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
-- Name: claims_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -54,8 +125,8 @@ CREATE SEQUENCE public.claims_id_seq
|
|||||||
ALTER SEQUENCE public.claims_id_seq OWNER TO admin;
|
ALTER SEQUENCE public.claims_id_seq OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3465 (class 0 OID 0)
|
-- TOC entry 3608 (class 0 OID 0)
|
||||||
-- Dependencies: 223
|
-- Dependencies: 224
|
||||||
-- Name: claims_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
-- Name: claims_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -63,20 +134,268 @@ ALTER SEQUENCE public.claims_id_seq OWNED BY public.claims.id;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 225 (class 1259 OID 16439)
|
-- TOC entry 239 (class 1259 OID 19919)
|
||||||
|
-- Name: collection_games; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.collection_games (
|
||||||
|
collection_id bigint NOT NULL,
|
||||||
|
game_id bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.collection_games OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 238 (class 1259 OID 19905)
|
||||||
|
-- Name: collections; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.collections (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
user_id bigint NOT NULL,
|
||||||
|
name character varying(60) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.collections OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 237 (class 1259 OID 19904)
|
||||||
|
-- Name: collections_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.collections_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.collections_id_seq OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3609 (class 0 OID 0)
|
||||||
|
-- Dependencies: 237
|
||||||
|
-- Name: collections_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.collections_id_seq OWNED BY public.collections.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 241 (class 1259 OID 19940)
|
||||||
|
-- Name: comments; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.comments (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
created_by_user_id bigint NOT NULL,
|
||||||
|
created_at timestamp(6) with time zone DEFAULT now() NOT NULL,
|
||||||
|
content text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.comments OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 240 (class 1259 OID 19939)
|
||||||
|
-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.comments_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.comments_id_seq OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3610 (class 0 OID 0)
|
||||||
|
-- Dependencies: 240
|
||||||
|
-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 232 (class 1259 OID 19848)
|
||||||
|
-- Name: games; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.games (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
name character varying(60) CONSTRAINT games_names_not_null NOT NULL,
|
||||||
|
image_path text,
|
||||||
|
bgg_id bigint
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.games OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 231 (class 1259 OID 19847)
|
||||||
|
-- Name: games_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.games_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.games_id_seq OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3611 (class 0 OID 0)
|
||||||
|
-- Dependencies: 231
|
||||||
|
-- Name: games_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.games_id_seq OWNED BY public.games.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 242 (class 1259 OID 19959)
|
||||||
|
-- Name: match_comments; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.match_comments (
|
||||||
|
match_id bigint NOT NULL,
|
||||||
|
comment_id bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.match_comments OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 235 (class 1259 OID 19872)
|
||||||
|
-- Name: match_players; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.match_players (
|
||||||
|
match_id bigint NOT NULL,
|
||||||
|
player_id bigint NOT NULL,
|
||||||
|
standing integer NOT NULL,
|
||||||
|
elo_change integer NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.match_players OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 234 (class 1259 OID 19859)
|
||||||
|
-- Name: matches; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.matches (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
game_id bigint NOT NULL,
|
||||||
|
owning_user_id bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.matches OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 233 (class 1259 OID 19858)
|
||||||
|
-- Name: matches_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.matches_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.matches_id_seq OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3612 (class 0 OID 0)
|
||||||
|
-- Dependencies: 233
|
||||||
|
-- Name: matches_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.matches_id_seq OWNED BY public.matches.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 236 (class 1259 OID 19889)
|
||||||
|
-- Name: player_circles; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.player_circles (
|
||||||
|
player_id bigint NOT NULL,
|
||||||
|
circle_id bigint NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.player_circles OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 228 (class 1259 OID 19809)
|
||||||
|
-- Name: players; Type: TABLE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE TABLE public.players (
|
||||||
|
id bigint NOT NULL,
|
||||||
|
name text NOT NULL,
|
||||||
|
elo integer DEFAULT 1000 NOT NULL,
|
||||||
|
is_rating_locked boolean DEFAULT false NOT NULL,
|
||||||
|
can_be_multiple boolean DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TABLE public.players OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 227 (class 1259 OID 19808)
|
||||||
|
-- Name: players_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.players_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.players_id_seq OWNER TO admin;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3613 (class 0 OID 0)
|
||||||
|
-- Dependencies: 227
|
||||||
|
-- Name: players_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.players_id_seq OWNED BY public.players.id;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 226 (class 1259 OID 16439)
|
||||||
-- Name: user_claims; Type: TABLE; Schema: public; Owner: admin
|
-- Name: user_claims; Type: TABLE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE public.user_claims (
|
CREATE TABLE public.user_claims (
|
||||||
userid bigint,
|
user_id bigint,
|
||||||
claimid integer
|
claim_id integer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.user_claims OWNER TO admin;
|
ALTER TABLE public.user_claims OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 222 (class 1259 OID 16392)
|
-- TOC entry 223 (class 1259 OID 16392)
|
||||||
-- Name: users; Type: TABLE; Schema: public; Owner: admin
|
-- Name: users; Type: TABLE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -85,14 +404,19 @@ CREATE TABLE public.users (
|
|||||||
username character varying(20) NOT NULL,
|
username character varying(20) NOT NULL,
|
||||||
pass_hash text NOT NULL,
|
pass_hash text NOT NULL,
|
||||||
is_active boolean DEFAULT true CONSTRAINT users_active_not_null NOT NULL,
|
is_active boolean DEFAULT true CONSTRAINT users_active_not_null NOT NULL,
|
||||||
is_admin boolean DEFAULT false NOT NULL
|
is_admin boolean DEFAULT false NOT NULL,
|
||||||
|
player_id bigint NOT NULL,
|
||||||
|
failed_attempts integer DEFAULT 0 NOT NULL,
|
||||||
|
last_login_attempt date,
|
||||||
|
require_new_password boolean DEFAULT false NOT NULL,
|
||||||
|
refresh_count bigint DEFAULT 0 NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE public.users OWNER TO admin;
|
ALTER TABLE public.users OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 219 (class 1259 OID 16389)
|
-- TOC entry 220 (class 1259 OID 16389)
|
||||||
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -107,8 +431,8 @@ CREATE SEQUENCE public.users_id_seq
|
|||||||
ALTER SEQUENCE public.users_id_seq OWNER TO admin;
|
ALTER SEQUENCE public.users_id_seq OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3466 (class 0 OID 0)
|
-- TOC entry 3614 (class 0 OID 0)
|
||||||
-- Dependencies: 219
|
-- Dependencies: 220
|
||||||
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -116,7 +440,7 @@ ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 221 (class 1259 OID 16391)
|
-- TOC entry 222 (class 1259 OID 16391)
|
||||||
-- Name: users_pass_hash_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
-- Name: users_pass_hash_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -131,8 +455,8 @@ CREATE SEQUENCE public.users_pass_hash_seq
|
|||||||
ALTER SEQUENCE public.users_pass_hash_seq OWNER TO admin;
|
ALTER SEQUENCE public.users_pass_hash_seq OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3467 (class 0 OID 0)
|
-- TOC entry 3615 (class 0 OID 0)
|
||||||
-- Dependencies: 221
|
-- Dependencies: 222
|
||||||
-- Name: users_pass_hash_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
-- Name: users_pass_hash_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -140,7 +464,7 @@ ALTER SEQUENCE public.users_pass_hash_seq OWNED BY public.users.pass_hash;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 220 (class 1259 OID 16390)
|
-- TOC entry 221 (class 1259 OID 16390)
|
||||||
-- Name: users_username_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
-- Name: users_username_seq; Type: SEQUENCE; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -155,8 +479,8 @@ CREATE SEQUENCE public.users_username_seq
|
|||||||
ALTER SEQUENCE public.users_username_seq OWNER TO admin;
|
ALTER SEQUENCE public.users_username_seq OWNER TO admin;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3468 (class 0 OID 0)
|
-- TOC entry 3616 (class 0 OID 0)
|
||||||
-- Dependencies: 220
|
-- Dependencies: 221
|
||||||
-- Name: users_username_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
-- Name: users_username_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -164,7 +488,15 @@ ALTER SEQUENCE public.users_username_seq OWNED BY public.users.username;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3305 (class 2604 OID 16434)
|
-- TOC entry 3412 (class 2604 OID 19840)
|
||||||
|
-- Name: circles id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.circles ALTER COLUMN id SET DEFAULT nextval('public.circles_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3406 (class 2604 OID 20726)
|
||||||
-- Name: claims id; Type: DEFAULT; Schema: public; Owner: admin
|
-- Name: claims id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -172,7 +504,47 @@ ALTER TABLE ONLY public.claims ALTER COLUMN id SET DEFAULT nextval('public.claim
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3300 (class 2604 OID 16395)
|
-- TOC entry 3417 (class 2604 OID 19908)
|
||||||
|
-- Name: collections id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.collections ALTER COLUMN id SET DEFAULT nextval('public.collections_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3418 (class 2604 OID 19943)
|
||||||
|
-- Name: comments id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3415 (class 2604 OID 19851)
|
||||||
|
-- Name: games id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.games ALTER COLUMN id SET DEFAULT nextval('public.games_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3416 (class 2604 OID 19862)
|
||||||
|
-- Name: matches id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.matches ALTER COLUMN id SET DEFAULT nextval('public.matches_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3408 (class 2604 OID 19812)
|
||||||
|
-- Name: players id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.players ALTER COLUMN id SET DEFAULT nextval('public.players_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3398 (class 2604 OID 16395)
|
||||||
-- Name: users id; Type: DEFAULT; Schema: public; Owner: admin
|
-- Name: users id; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -180,7 +552,7 @@ ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3301 (class 2604 OID 16405)
|
-- TOC entry 3399 (class 2604 OID 16405)
|
||||||
-- Name: users username; Type: DEFAULT; Schema: public; Owner: admin
|
-- Name: users username; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -188,7 +560,7 @@ ALTER TABLE ONLY public.users ALTER COLUMN username SET DEFAULT nextval('public.
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3302 (class 2604 OID 16411)
|
-- TOC entry 3400 (class 2604 OID 16411)
|
||||||
-- Name: users pass_hash; Type: DEFAULT; Schema: public; Owner: admin
|
-- Name: users pass_hash; Type: DEFAULT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -196,7 +568,16 @@ ALTER TABLE ONLY public.users ALTER COLUMN pass_hash SET DEFAULT nextval('public
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3310 (class 2606 OID 16438)
|
-- TOC entry 3427 (class 2606 OID 19846)
|
||||||
|
-- Name: circles circles_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.circles
|
||||||
|
ADD CONSTRAINT circles_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3423 (class 2606 OID 20728)
|
||||||
-- Name: claims claims_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
-- Name: claims claims_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -205,7 +586,52 @@ ALTER TABLE ONLY public.claims
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3308 (class 2606 OID 16403)
|
-- TOC entry 3433 (class 2606 OID 19913)
|
||||||
|
-- Name: collections collections_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.collections
|
||||||
|
ADD CONSTRAINT collections_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3435 (class 2606 OID 19952)
|
||||||
|
-- Name: comments comments_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments
|
||||||
|
ADD CONSTRAINT comments_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3429 (class 2606 OID 19857)
|
||||||
|
-- Name: games games_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.games
|
||||||
|
ADD CONSTRAINT games_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3431 (class 2606 OID 19866)
|
||||||
|
-- Name: matches matches_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.matches
|
||||||
|
ADD CONSTRAINT matches_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3425 (class 2606 OID 19824)
|
||||||
|
-- Name: players players_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.players
|
||||||
|
ADD CONSTRAINT players_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3421 (class 2606 OID 16403)
|
||||||
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -214,26 +640,169 @@ ALTER TABLE ONLY public.users
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3311 (class 2606 OID 16447)
|
-- TOC entry 3452 (class 2606 OID 19979)
|
||||||
|
-- Name: circle_comments circle_comments_circles_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.circle_comments
|
||||||
|
ADD CONSTRAINT circle_comments_circles_fkey FOREIGN KEY (circle_id) REFERENCES public.circles(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3453 (class 2606 OID 19984)
|
||||||
|
-- Name: circle_comments circle_comments_comment_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.circle_comments
|
||||||
|
ADD CONSTRAINT circle_comments_comment_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3439 (class 2606 OID 20708)
|
||||||
|
-- Name: circles circles_users_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.circles
|
||||||
|
ADD CONSTRAINT circles_users_fkey FOREIGN KEY (owning_user_id) REFERENCES public.users(id) NOT VALID;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3447 (class 2606 OID 19924)
|
||||||
|
-- Name: collection_games collection_games_collections_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.collection_games
|
||||||
|
ADD CONSTRAINT collection_games_collections_fkey FOREIGN KEY (collection_id) REFERENCES public.collections(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3448 (class 2606 OID 19929)
|
||||||
|
-- Name: collection_games collection_games_games_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.collection_games
|
||||||
|
ADD CONSTRAINT collection_games_games_fkey FOREIGN KEY (game_id) REFERENCES public.games(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3446 (class 2606 OID 19914)
|
||||||
|
-- Name: collections collections_users_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.collections
|
||||||
|
ADD CONSTRAINT collections_users_fkey FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3449 (class 2606 OID 19953)
|
||||||
|
-- Name: comments comments_users_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.comments
|
||||||
|
ADD CONSTRAINT comments_users_fkey FOREIGN KEY (created_by_user_id) REFERENCES public.users(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3450 (class 2606 OID 19969)
|
||||||
|
-- Name: match_comments match_comments_comments_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.match_comments
|
||||||
|
ADD CONSTRAINT match_comments_comments_fkey FOREIGN KEY (comment_id) REFERENCES public.comments(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3451 (class 2606 OID 19964)
|
||||||
|
-- Name: match_comments match_comments_matches_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.match_comments
|
||||||
|
ADD CONSTRAINT match_comments_matches_fkey FOREIGN KEY (match_id) REFERENCES public.matches(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3442 (class 2606 OID 19884)
|
||||||
|
-- Name: match_players match_players_matches_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.match_players
|
||||||
|
ADD CONSTRAINT match_players_matches_fkey FOREIGN KEY (match_id) REFERENCES public.matches(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3443 (class 2606 OID 19879)
|
||||||
|
-- Name: match_players match_players_players_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.match_players
|
||||||
|
ADD CONSTRAINT match_players_players_fkey FOREIGN KEY (player_id) REFERENCES public.players(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3440 (class 2606 OID 19867)
|
||||||
|
-- Name: matches matches_games_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.matches
|
||||||
|
ADD CONSTRAINT matches_games_fkey FOREIGN KEY (game_id) REFERENCES public.games(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3441 (class 2606 OID 20721)
|
||||||
|
-- Name: matches matches_users_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.matches
|
||||||
|
ADD CONSTRAINT matches_users_fkey FOREIGN KEY (owning_user_id) REFERENCES public.users(id) NOT VALID;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3444 (class 2606 OID 19899)
|
||||||
|
-- Name: player_circles player_circles_circles_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.player_circles
|
||||||
|
ADD CONSTRAINT player_circles_circles_fkey FOREIGN KEY (circle_id) REFERENCES public.circles(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3445 (class 2606 OID 19894)
|
||||||
|
-- Name: player_circles player_circles_players_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.player_circles
|
||||||
|
ADD CONSTRAINT player_circles_players_fkey FOREIGN KEY (player_id) REFERENCES public.players(id);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 3437 (class 2606 OID 20730)
|
||||||
-- Name: user_claims user_claims_claimid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
-- Name: user_claims user_claims_claimid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public.user_claims
|
ALTER TABLE ONLY public.user_claims
|
||||||
ADD CONSTRAINT user_claims_claimid_fkey FOREIGN KEY (claimid) REFERENCES public.claims(id);
|
ADD CONSTRAINT user_claims_claimid_fkey FOREIGN KEY (claim_id) REFERENCES public.claims(id);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 3312 (class 2606 OID 16442)
|
-- TOC entry 3438 (class 2606 OID 16442)
|
||||||
-- Name: user_claims user_claims_userid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
-- Name: user_claims user_claims_userid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY public.user_claims
|
ALTER TABLE ONLY public.user_claims
|
||||||
ADD CONSTRAINT user_claims_userid_fkey FOREIGN KEY (userid) REFERENCES public.users(id);
|
ADD CONSTRAINT user_claims_userid_fkey FOREIGN KEY (user_id) REFERENCES public.users(id);
|
||||||
|
|
||||||
|
|
||||||
-- Completed on 2026-02-13 18:14:43 GMT
|
--
|
||||||
|
-- TOC entry 3436 (class 2606 OID 19830)
|
||||||
|
-- Name: users users_players_fkey; Type: FK CONSTRAINT; Schema: public; Owner: admin
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.users
|
||||||
|
ADD CONSTRAINT users_players_fkey FOREIGN KEY (player_id) REFERENCES public.players(id) NOT VALID;
|
||||||
|
|
||||||
|
|
||||||
|
-- Completed on 2026-02-18 20:07:03 GMT
|
||||||
|
|
||||||
--
|
--
|
||||||
-- PostgreSQL database dump complete
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,103 @@
|
|||||||
import {orm} from "../orm/orm";
|
import { orm } from '../orm/orm';
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from 'jsonwebtoken';
|
||||||
import {UnwrappedRequest} from "../utilities/guard";
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
import {ErrorResponse} from "../utilities/responseHelper";
|
import { ErrorResponse, OkResponse, UnauthorizedResponse } from '../utilities/responseHelper';
|
||||||
import {Claims} from "../orm/claims";
|
import { Claims } from '../orm/claims';
|
||||||
import {UnauthorizedError} from "../utilities/errors";
|
import { UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { ChangePasswordRequest, LoginRequest, SecureId } from '../utilities/requestModels';
|
||||||
|
|
||||||
async function login(request: UnwrappedRequest): Promise<Response> {
|
async function login(request: UnwrappedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const requestBody = request.json;
|
const requestBody = request.body as LoginRequest;
|
||||||
console.log(`/api/auth/login: username=${requestBody.username}`);
|
const verify: {
|
||||||
const claims: Claims | null = await orm.users.verify(requestBody.username, requestBody.password);
|
userId: SecureId;
|
||||||
console.log(claims);
|
refreshCount: string;
|
||||||
if (claims) {
|
} | null = await orm.users.verifyCredentials(requestBody.username, requestBody.password);
|
||||||
const token = jwt.sign({...claims}, process.env.JWT_SECRET_KEY as string, {expiresIn: "24h"});
|
if (!verify) {
|
||||||
return Response.json({token: token, claims: claims}, {status: 200});
|
throw new UnauthorizedError('Invalid credentials');
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new UnauthorizedError('Invalid credentials');
|
// Build refresh token that expires in 30 days, return as secure HTTP only cookie.
|
||||||
|
const tokenLifeSpanInDays = 30;
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
u: verify.userId.raw,
|
||||||
|
r: verify.refreshCount,
|
||||||
|
},
|
||||||
|
process.env.JWT_SECRET_KEY as string,
|
||||||
|
{ expiresIn: `${tokenLifeSpanInDays * 24}h` },
|
||||||
|
);
|
||||||
|
const cookies = request?.request?.cookies;
|
||||||
|
cookies?.set({
|
||||||
|
name: 'refresh',
|
||||||
|
value: token,
|
||||||
|
httpOnly: true,
|
||||||
|
secure: true,
|
||||||
|
maxAge: tokenLifeSpanInDays * 24 * 60 * 60,
|
||||||
|
});
|
||||||
|
return new OkResponse();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function test(request: UnwrappedRequest) {
|
|
||||||
return Response.json(request.claims, {status: 200});
|
async function token(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const cookies = request.request.cookies;
|
||||||
|
const refreshCookie = cookies.get('refresh');
|
||||||
|
if (!refreshCookie) {
|
||||||
|
throw new UnauthorizedError('No refresh token found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshToken: {
|
||||||
|
u: string;
|
||||||
|
r: string;
|
||||||
|
} = jwt.verify(refreshCookie, process.env.JWT_SECRET_KEY as string) as { u: string; r: string };
|
||||||
|
|
||||||
|
if (!(await orm.users.verifyRefreshCount(SecureId.fromID(refreshToken.u), refreshToken.r))) {
|
||||||
|
const response = new UnauthorizedResponse('Invalid refresh token');
|
||||||
|
response.headers.set('Clear-Site-Data', '"cookies","cache","storage","executionContexts"');
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
const claims: Claims | null = await orm.claims.getByUserId(refreshToken.u);
|
||||||
|
|
||||||
|
const token = jwt.sign({ ...claims }, process.env.JWT_SECRET_KEY as string, { expiresIn: '1h' });
|
||||||
|
return new OkResponse({ token });
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const response = new OkResponse();
|
||||||
|
response.headers.set('Clear-Site-Data', '"cookies","cache","storage","executionContexts"');
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePassword(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const requestBody = request.body as ChangePasswordRequest;
|
||||||
|
return new OkResponse(
|
||||||
|
await orm.users.changePassword(
|
||||||
|
SecureId.fromHash(request.params.id),
|
||||||
|
requestBody.oldPassword,
|
||||||
|
requestBody.newPassword,
|
||||||
|
request.claims,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
login,
|
login,
|
||||||
test
|
token,
|
||||||
|
logout,
|
||||||
|
changePassword,
|
||||||
};
|
};
|
||||||
70
src/endpoints/games.ts
Normal file
70
src/endpoints/games.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
|
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
|
||||||
|
import {
|
||||||
|
CreateGameRequest,
|
||||||
|
SecureId,
|
||||||
|
UpdateGameRequest,
|
||||||
|
} from '../utilities/requestModels';
|
||||||
|
|
||||||
|
async function create(request: UnwrappedRequest<CreateGameRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const newUser = await orm.games.create(
|
||||||
|
{
|
||||||
|
name: request.body.name,
|
||||||
|
bggId: request.body.bggId,
|
||||||
|
imagePath: request.body.imagePath,
|
||||||
|
},
|
||||||
|
request.claims,
|
||||||
|
);
|
||||||
|
return new CreatedResponse(newUser);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.games.get(SecureId.fromHash(request.params.id)));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(request: UnwrappedRequest<UpdateGameRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(
|
||||||
|
await orm.games.update(
|
||||||
|
SecureId.fromHash(request.params.id),
|
||||||
|
request.body,
|
||||||
|
request.claims,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drop(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.games.drop(SecureId.fromHash(request.params.id)));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function query(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.games.query(request.params.query));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
update,
|
||||||
|
drop,
|
||||||
|
query,
|
||||||
|
};
|
||||||
46
src/endpoints/player.ts
Normal file
46
src/endpoints/player.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
|
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
|
||||||
|
import { CreatePlayerRequest, SecureId, UpdatePlayerRequest } from '../utilities/requestModels';
|
||||||
|
|
||||||
|
async function create(request: UnwrappedRequest<CreatePlayerRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const newPlayer = await orm.players.create(request.body, request.claims);
|
||||||
|
return new CreatedResponse(newPlayer);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.players.get(SecureId.fromHash(request.params.id), request.claims));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(request: UnwrappedRequest<UpdatePlayerRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(
|
||||||
|
await orm.players.update(SecureId.fromHash(request.params.id), request.body, request.claims),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drop(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.players.drop(SecureId.fromHash(request.params.id), request.claims));
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
create,
|
||||||
|
get,
|
||||||
|
update,
|
||||||
|
drop,
|
||||||
|
};
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
import {orm} from "../orm/orm";
|
import { orm } from '../orm/orm';
|
||||||
import {UnwrappedRequest} from "../utilities/guard";
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
import {ErrorResponse} from "../utilities/responseHelper";
|
import { CreatedResponse, ErrorResponse, OkResponse } from '../utilities/responseHelper';
|
||||||
|
import { CreateUserRequest, SecureId, UpdateUserRequest } from '../utilities/requestModels';
|
||||||
|
|
||||||
async function create(request: UnwrappedRequest): Promise<Response> {
|
async function create(request: UnwrappedRequest<CreateUserRequest>): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
const requestBody = request.json;
|
const newUser = await orm.users.create(
|
||||||
|
|
||||||
const newUser = await orm.users.create(requestBody.username, requestBody.password, request.claims);
|
|
||||||
if(!newUser) {
|
|
||||||
return new Response(null,{status: 201})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.json(
|
|
||||||
{
|
{
|
||||||
...newUser
|
...request.body,
|
||||||
|
playerId: SecureId.fromHash(request.body.playerId),
|
||||||
},
|
},
|
||||||
{status: 201}
|
request.claims,
|
||||||
);
|
);
|
||||||
|
return new CreatedResponse(newUser);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
@@ -24,9 +20,29 @@ async function create(request: UnwrappedRequest): Promise<Response> {
|
|||||||
|
|
||||||
async function get(request: UnwrappedRequest): Promise<Response> {
|
async function get(request: UnwrappedRequest): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
return Response.json({
|
return new OkResponse(await orm.users.get(SecureId.fromHash(request.params.id), request.claims));
|
||||||
...(await orm.users.get(request.params.id, request.claims))
|
} catch (error: any) {
|
||||||
}, {status: 200});
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(request: UnwrappedRequest<UpdateUserRequest>): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(
|
||||||
|
await orm.users.update(
|
||||||
|
SecureId.fromHash(request.params.id),
|
||||||
|
request.body,
|
||||||
|
request.claims,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
return new ErrorResponse(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function drop(request: UnwrappedRequest): Promise<Response> {
|
||||||
|
try {
|
||||||
|
return new OkResponse(await orm.users.drop(SecureId.fromHash(request.params.id), request.claims));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
@@ -35,4 +51,6 @@ async function get(request: UnwrappedRequest): Promise<Response> {
|
|||||||
export default {
|
export default {
|
||||||
create,
|
create,
|
||||||
get,
|
get,
|
||||||
}
|
update,
|
||||||
|
drop,
|
||||||
|
};
|
||||||
|
|||||||
27
src/index.ts
27
src/index.ts
@@ -1,26 +1,25 @@
|
|||||||
import {unwrapMethod, guard} from './utilities/guard';
|
import auth from './routes/auth';
|
||||||
import auth from "./endpoints/auth";
|
import user from './routes/user';
|
||||||
import user from "./endpoints/user";
|
import player from './routes/player';
|
||||||
|
import game from './routes/game';
|
||||||
|
import { OkResponse } from './utilities/responseHelper';
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
routes: {
|
routes: {
|
||||||
"/api/auth/login": {
|
...auth,
|
||||||
POST: unwrapMethod(auth.login),
|
...user,
|
||||||
|
...player,
|
||||||
|
...game,
|
||||||
|
'/test': {
|
||||||
|
GET: (request) => {
|
||||||
|
return new OkResponse();
|
||||||
},
|
},
|
||||||
"/api/auth/test": {
|
|
||||||
GET: guard(auth.test, ['ADMIN', 'USERS_OTHER_DELETE'])
|
|
||||||
},
|
|
||||||
"/api/user": {
|
|
||||||
POST: guard(user.create, ['ADMIN', 'USERS_CREATE'])
|
|
||||||
},
|
|
||||||
"/api/user/:id": {
|
|
||||||
GET: guard(user.get, ['ADMIN', 'USERS_OTHERS_READ', 'USERS_SELF_READ'])
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// (optional) fallback for unmatched routes:
|
// (optional) fallback for unmatched routes:
|
||||||
fetch(): Response {
|
fetch(): Response {
|
||||||
return Response.json({message: "Not found"}, {status: 404});
|
return Response.json({ message: 'Not found' }, { status: 404 });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { sql } from 'bun';
|
import { sql } from 'bun';
|
||||||
|
import { ClaimDefinition } from '../utilities/claimDefinitions';
|
||||||
|
|
||||||
export class Claims {
|
export class Claims extends ClaimDefinition {
|
||||||
userId: string | undefined;
|
userId?: string;
|
||||||
claims: string[] = [];
|
claims: string[] = [];
|
||||||
|
|
||||||
public static test(userClaims: Claims, guardClaim: string): Boolean {
|
public static test(guardClaim: string, userClaims?: Claims): Boolean {
|
||||||
return userClaims.claims.some(x => x === guardClaim);
|
return userClaims === undefined || userClaims.claims.some((x) => x === guardClaim);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,11 +14,11 @@ export class ClaimsOrm {
|
|||||||
async getByUserId(userId: string): Promise<Claims> {
|
async getByUserId(userId: string): Promise<Claims> {
|
||||||
const dbResults: any[] = await sql`SELECT c.name
|
const dbResults: any[] = await sql`SELECT c.name
|
||||||
from user_claims as uc
|
from user_claims as uc
|
||||||
JOIN claims as c on uc.claimid = c.id
|
JOIN claims as c on uc.claim_id = c.id
|
||||||
where uc.userid = ${userId};`;
|
where uc.user_id = ${userId};`;
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.userId = userId;
|
claims.userId = userId;
|
||||||
claims.claims = dbResults.map(x => x.name);
|
claims.claims = dbResults.map((x) => x.name);
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +26,6 @@ export class ClaimsOrm {
|
|||||||
const dbResults: any[] = await sql`SELECT id
|
const dbResults: any[] = await sql`SELECT id
|
||||||
FROM claims
|
FROM claims
|
||||||
WHERE is_default = true;`;
|
WHERE is_default = true;`;
|
||||||
return dbResults.map(x => x.id);
|
return dbResults.map((x) => x.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
125
src/orm/games.ts
Normal file
125
src/orm/games.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import { Claims } from './claims';
|
||||||
|
import { sql } from 'bun';
|
||||||
|
import { first, memoize } from 'lodash';
|
||||||
|
import { NotFoundError, UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { CreateGameRequest, SecureId, UpdateGameRequest } from '../utilities/requestModels';
|
||||||
|
import { memo } from '../utilities/helpers';
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
id: SecureId;
|
||||||
|
name: string;
|
||||||
|
imagePath?: string;
|
||||||
|
bggId?: string;
|
||||||
|
|
||||||
|
constructor(input: { id: SecureId; name: string; imagePath?: string; bggId?: string }) {
|
||||||
|
this.id = input.id;
|
||||||
|
this.name = input?.name;
|
||||||
|
this.imagePath = input?.imagePath;
|
||||||
|
this.bggId = input?.bggId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GamesOrm {
|
||||||
|
async create(model: CreateGameRequest, claims?: Claims): Promise<Game | null> {
|
||||||
|
await sql`INSERT INTO games (name, image_path, bgg_id)
|
||||||
|
VALUES (${model.name}, ${Claims.test(Claims.GAMES.MANAGE_IMAGES, claims) ? model.imagePath : null}, ${model.bggId})`;
|
||||||
|
const newGameId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.get(SecureId.fromID(newGameId));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: SecureId): Promise<Game> {
|
||||||
|
const dbResult: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM games
|
||||||
|
WHERE id = ${id.raw}
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dbResult) {
|
||||||
|
throw new NotFoundError('No matching game exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Game({
|
||||||
|
id: SecureId.fromID(dbResult.id),
|
||||||
|
name: dbResult.name,
|
||||||
|
bggId: dbResult.bgg_id,
|
||||||
|
imagePath: dbResult.image_path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: SecureId, patch: UpdateGameRequest, claims?: Claims): Promise<Game | null> {
|
||||||
|
const gameToUpdate = await this.get(id);
|
||||||
|
gameToUpdate.name = patch.name ?? gameToUpdate.name;
|
||||||
|
gameToUpdate.bggId = patch.bggId ?? gameToUpdate.bggId;
|
||||||
|
|
||||||
|
if (Claims.test(Claims.GAMES.MANAGE_IMAGES, claims)) {
|
||||||
|
gameToUpdate.imagePath = patch.imagePath ?? gameToUpdate.imagePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`UPDATE games
|
||||||
|
SET name=${gameToUpdate.name},
|
||||||
|
bgg_id=${gameToUpdate.bggId},
|
||||||
|
image_path=${gameToUpdate.imagePath}
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.get(id);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async drop(id: SecureId): Promise<undefined> {
|
||||||
|
// Ensure player exists before attempting to delete
|
||||||
|
await this.get(id);
|
||||||
|
await sql.transaction(async (tx) => {
|
||||||
|
await tx`DELETE
|
||||||
|
FROM collection_games
|
||||||
|
WHERE game_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM match_players
|
||||||
|
WHERE match_id IN (SELECT id FROM matches WHERE game_id = ${id.raw})`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM matches
|
||||||
|
WHERE game_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM games
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
query: (query: string) => Promise<Game[]> = memo<(query: string) => Promise<Game[]>,Game[]>(this.#query);
|
||||||
|
async #query(query: string): Promise<Game[]> {
|
||||||
|
const dbResult: any = await sql` SELECT
|
||||||
|
id, name
|
||||||
|
FROM (SELECT *, SIMILARITY(${query}, name) as similarity FROM games)
|
||||||
|
WHERE similarity > 0
|
||||||
|
ORDER BY similarity
|
||||||
|
LIMIT 5;`;
|
||||||
|
|
||||||
|
if (!dbResult) {
|
||||||
|
throw new NotFoundError('No matching game exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbResult.map(
|
||||||
|
(x: { id: string; name: string }) =>
|
||||||
|
new Game({
|
||||||
|
id: SecureId.fromID(x.id),
|
||||||
|
name: x.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
import {ClaimsOrm} from "./claims";
|
import { ClaimsOrm } from './claims';
|
||||||
import {UsersOrm} from "./user";
|
import { UsersOrm } from './user';
|
||||||
|
import { PlayersOrm } from './players';
|
||||||
|
import { GamesOrm } from './games';
|
||||||
|
|
||||||
class Orm {
|
class Orm {
|
||||||
claims: ClaimsOrm;
|
readonly claims: ClaimsOrm = new ClaimsOrm();
|
||||||
users: UsersOrm;
|
readonly users: UsersOrm = new UsersOrm();
|
||||||
|
readonly players: PlayersOrm = new PlayersOrm();
|
||||||
constructor() {
|
readonly games: GamesOrm = new GamesOrm();
|
||||||
this.claims = new ClaimsOrm();
|
|
||||||
this.users = new UsersOrm(this.claims);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const orm = new Orm();
|
export const orm = new Orm();
|
||||||
140
src/orm/players.ts
Normal file
140
src/orm/players.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { Claims } from './claims';
|
||||||
|
import { sql } from 'bun';
|
||||||
|
import { first } from 'lodash';
|
||||||
|
import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { orm } from './orm';
|
||||||
|
import { SecureId, UpdatePlayerRequest } from '../utilities/requestModels';
|
||||||
|
|
||||||
|
export class Player {
|
||||||
|
id: SecureId;
|
||||||
|
name: string;
|
||||||
|
elo: number;
|
||||||
|
isRatingLocked: boolean;
|
||||||
|
canBeMultiple: boolean;
|
||||||
|
|
||||||
|
constructor(input: {
|
||||||
|
id: SecureId;
|
||||||
|
name: string;
|
||||||
|
elo?: number;
|
||||||
|
isRatingLocked?: boolean;
|
||||||
|
canBeMultiple?: boolean;
|
||||||
|
}) {
|
||||||
|
this.id = input.id;
|
||||||
|
this.name = input?.name;
|
||||||
|
this.elo = input?.elo ?? 1000;
|
||||||
|
this.isRatingLocked = input?.isRatingLocked ?? false;
|
||||||
|
this.canBeMultiple = input?.canBeMultiple ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayersOrm {
|
||||||
|
async create(model: {name: string}, claims?: Claims): Promise<Player | null> {
|
||||||
|
await sql`INSERT INTO players (name)
|
||||||
|
VALUES (${model.name})`;
|
||||||
|
const newPlayerId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.get(SecureId.fromID(newPlayerId), claims);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: SecureId, claims?: Claims): Promise<Player> {
|
||||||
|
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.READ, claims))) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
} else if (Claims.test(Claims.PLAYERS.SELF.READ, claims) && claims?.userId) {
|
||||||
|
const user = await orm.users.get(SecureId.fromHash(claims.userId));
|
||||||
|
if (id.raw !== user.playerId.raw) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const dbResult: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM players
|
||||||
|
WHERE id = ${id.raw}
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!dbResult) {
|
||||||
|
throw new NotFoundError('No matching player exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Player({
|
||||||
|
id: SecureId.fromID(dbResult.id),
|
||||||
|
name: dbResult.name,
|
||||||
|
elo: dbResult.elo,
|
||||||
|
isRatingLocked: dbResult.is_rating_locked,
|
||||||
|
canBeMultiple: dbResult.can_be_multiple,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: SecureId,
|
||||||
|
patch: {
|
||||||
|
name?: string;
|
||||||
|
isRatingLocked?: boolean;
|
||||||
|
canBeMultiple?: boolean;
|
||||||
|
},
|
||||||
|
claims?: Claims,
|
||||||
|
): Promise<Player | null> {
|
||||||
|
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.UPDATE, claims))) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
} else if (Claims.test(Claims.PLAYERS.SELF.UPDATE, claims) && claims?.userId) {
|
||||||
|
const user = await orm.users.get(SecureId.fromHash(claims.userId));
|
||||||
|
if (id.raw !== user.playerId.raw) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerToUpdate = await this.get(id);
|
||||||
|
playerToUpdate.name = patch.name ?? playerToUpdate.name;
|
||||||
|
playerToUpdate.isRatingLocked = patch.isRatingLocked ?? playerToUpdate.isRatingLocked;
|
||||||
|
playerToUpdate.canBeMultiple = patch.canBeMultiple ?? playerToUpdate.canBeMultiple;
|
||||||
|
|
||||||
|
await sql`UPDATE players
|
||||||
|
SET name=${playerToUpdate.name},
|
||||||
|
is_rating_locked=${playerToUpdate.isRatingLocked},
|
||||||
|
can_be_multiple=${playerToUpdate.canBeMultiple}
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.get(id, claims);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async drop(id: SecureId, claims?: Claims): Promise<undefined> {
|
||||||
|
if (!(Claims.test(Claims.ADMIN, claims) || Claims.test(Claims.PLAYERS.OTHER.DELETE, claims))) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
} else if (Claims.test(Claims.PLAYERS.SELF.DELETE, claims) && claims?.userId) {
|
||||||
|
const user = await orm.users.get(SecureId.fromHash(claims.userId));
|
||||||
|
if (id.raw !== user.playerId.raw) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure player exists before attempting to delete
|
||||||
|
await this.get(id);
|
||||||
|
await sql.transaction(async (tx) => {
|
||||||
|
await tx`DELETE
|
||||||
|
FROM player_circles
|
||||||
|
WHERE player_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM match_players
|
||||||
|
WHERE player_id = ${id.raw}`;
|
||||||
|
await tx`DELETE
|
||||||
|
FROM players
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/orm/user.ts
237
src/orm/user.ts
@@ -1,17 +1,21 @@
|
|||||||
import {Claims, ClaimsOrm} from "./claims";
|
import { Claims } from './claims';
|
||||||
import {sql} from "bun";
|
import { sql } from 'bun';
|
||||||
import {first} from "lodash";
|
import { first } from 'lodash';
|
||||||
import argon2 from "argon2";
|
import argon2 from 'argon2';
|
||||||
import {BadRequestError, NotFoundError, UnauthorizedError} from "../utilities/errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors';
|
||||||
|
import { SecureId, UpdateUserRequest } from '../utilities/requestModels';
|
||||||
|
import { orm } from './orm';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
id: string;
|
id: SecureId;
|
||||||
|
playerId: SecureId;
|
||||||
name: string;
|
name: string;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
constructor(id: string, name: string, isAdmin: boolean = false, isActive: boolean = true) {
|
constructor(id: SecureId, playerId: SecureId, name: string, isAdmin: boolean = false, isActive: boolean = true) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.playerId = playerId;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.isAdmin = isAdmin;
|
this.isAdmin = isAdmin;
|
||||||
this.isActive = isActive;
|
this.isActive = isActive;
|
||||||
@@ -19,79 +23,200 @@ export class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UsersOrm {
|
export class UsersOrm {
|
||||||
#claims: ClaimsOrm;
|
async create(
|
||||||
|
model: { username: string; password: string; playerId: SecureId },
|
||||||
constructor(claims: ClaimsOrm) {
|
claims?: Claims,
|
||||||
this.#claims = claims;
|
): Promise<User | null> {
|
||||||
|
const existingUser: any = first(
|
||||||
|
await sql`SELECT id
|
||||||
|
FROM users
|
||||||
|
WHERE username = ${model.username}
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
if (existingUser) {
|
||||||
|
throw new BadRequestError(`User ${model.username} already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(id: string, claims: Claims): Promise<User> {
|
const defaultClaims: number[] = await orm.claims.getDefaultClaims();
|
||||||
if (!(
|
const passwordHash = await argon2.hash(model.password);
|
||||||
Claims.test(claims, 'ADMIN') ||
|
await sql`INSERT INTO users (username, pass_hash, player_id)
|
||||||
Claims.test(claims, 'USERS_OTHER_READ') ||
|
VALUES (${model.username}, ${passwordHash}, ${model.playerId.raw})`;
|
||||||
(Claims.test(claims, 'USERS_SELF_READ') && id === claims.userId)
|
const newUserId: SecureId = SecureId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
|
||||||
)) {
|
await sql.transaction(async (tx) => {
|
||||||
throw new
|
for (let i in defaultClaims) {
|
||||||
UnauthorizedError();
|
await tx`INSERT INTO user_claims (user_id, claim_id)
|
||||||
|
VALUES (${newUserId.raw}, ${defaultClaims[i]})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this.get(newUserId, claims);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbResult: any = first(await sql`select *
|
async get(id: SecureId, claims?: Claims): Promise<User> {
|
||||||
from users
|
if (
|
||||||
where id = ${id}
|
!(
|
||||||
and is_active = true
|
Claims.test(Claims.ADMIN, claims) ||
|
||||||
limit 1`);
|
Claims.test(Claims.USERS.OTHER.READ, claims) ||
|
||||||
|
(Claims.test(Claims.USERS.SELF.READ, claims) && id.raw === claims?.userId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbResult: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = ${id.raw}
|
||||||
|
AND is_active = true
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
if (!dbResult) {
|
if (!dbResult) {
|
||||||
throw new NotFoundError('No matching user exists');
|
throw new NotFoundError('No matching user exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new User(dbResult.id, dbResult.username, dbResult.is_admin);
|
return new User(
|
||||||
|
SecureId.fromID(dbResult.id),
|
||||||
|
SecureId.fromID(dbResult.player_id),
|
||||||
|
dbResult.username,
|
||||||
|
dbResult.is_admin,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(username: string, password: string): Promise<Claims | null> {
|
async update(
|
||||||
|
id: SecureId,
|
||||||
|
patch: { isActive?: boolean; isAdmin?: boolean },
|
||||||
|
claims?: Claims,
|
||||||
|
): Promise<User | null> {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
Claims.test(Claims.ADMIN, claims) ||
|
||||||
|
Claims.test(Claims.USERS.OTHER.UPDATE, claims) ||
|
||||||
|
(Claims.test(Claims.USERS.SELF.UPDATE, claims) && id.raw === claims?.userId)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToUpdate = await this.get(id);
|
||||||
|
if (Claims.test(Claims.ADMIN, claims)) {
|
||||||
|
userToUpdate.isActive = patch.isActive ?? userToUpdate.isActive;
|
||||||
|
userToUpdate.isAdmin = patch.isAdmin ?? userToUpdate.isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sql`UPDATE users
|
||||||
|
SET is_active=${userToUpdate.isActive},
|
||||||
|
is_admin=${userToUpdate.isAdmin}
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dbResult: any = first(await sql`select *
|
return await this.get(id, claims);
|
||||||
from users
|
} catch (error) {
|
||||||
where username = ${username}
|
if (error instanceof UnauthorizedError) {
|
||||||
limit 1`);
|
|
||||||
if (!await argon2.verify(dbResult.pass_hash, password)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.#claims.getByUserId(dbResult.id);
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(username: string, password: string, claims: Claims): Promise<User | null> {
|
async drop(id: SecureId, claims?: Claims): Promise<User | null> {
|
||||||
const existingUser: any = first(await sql`SELECT id
|
if (
|
||||||
FROM users
|
!(
|
||||||
WHERE username = ${username}
|
Claims.test(Claims.ADMIN, claims) ||
|
||||||
LIMIT 1`);
|
Claims.test(Claims.USERS.OTHER.DELETE, claims) ||
|
||||||
if (existingUser) {
|
(Claims.test(Claims.USERS.SELF.DELETE, claims) && id.raw === claims?.userId)
|
||||||
throw new BadRequestError(`User ${username} already exists`);
|
)
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultClaims: number[] = await this.#claims.getDefaultClaims();
|
// Ensure user exists before attempting to delete
|
||||||
const passwordHash = await argon2.hash(password);
|
await this.get(id);
|
||||||
await sql`INSERT INTO users (username, pass_hash)
|
|
||||||
VALUES (${username}, ${passwordHash})`;
|
|
||||||
const newUserId: string = (first(await sql`SELECT lastval();`) as any)?.lastval as string;
|
|
||||||
await sql.transaction(async (tx) => {
|
await sql.transaction(async (tx) => {
|
||||||
for (let i in defaultClaims) {
|
await tx`DELETE
|
||||||
await tx`INSERT INTO user_claims (userid, claimid)
|
FROM user_claims
|
||||||
VALUES (${newUserId}, ${defaultClaims[i]})`;
|
WHERE user_id = ${id.raw}`;
|
||||||
}
|
await tx`DELETE
|
||||||
})
|
FROM users
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
});
|
||||||
|
|
||||||
if (!(
|
|
||||||
Claims.test(claims, 'ADMIN') ||
|
|
||||||
Claims.test(claims, 'USERS_OTHER_READ')
|
|
||||||
)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.get(newUserId, claims);
|
async verifyCredentials(
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<{ userId: SecureId; refreshCount: string } | null> {
|
||||||
|
const dbResult: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE username = ${username}
|
||||||
|
AND is_active = true
|
||||||
|
limit 1`,
|
||||||
|
);
|
||||||
|
if (!dbResult) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await argon2.verify(dbResult.pass_hash, password))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: SecureId.fromID(dbResult.id),
|
||||||
|
refreshCount: dbResult.refresh_count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyRefreshCount(id: SecureId, refreshCount: string): Promise<boolean> {
|
||||||
|
const dbResult: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = ${id.raw}
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
console.log(dbResult.refresh_count, refreshCount);
|
||||||
|
return dbResult.refresh_count === refreshCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePassword(
|
||||||
|
id: SecureId,
|
||||||
|
oldPassword: string | null,
|
||||||
|
newPassword: string,
|
||||||
|
claims?: Claims,
|
||||||
|
): Promise<undefined> {
|
||||||
|
const isAdmin = Claims.test(Claims.ADMIN, claims);
|
||||||
|
if (!(isAdmin || (Claims.test(Claims.USERS.SELF.UPDATE, claims) && id.raw === claims?.userId))) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin && oldPassword === null) {
|
||||||
|
throw new BadRequestError('Password is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbUser: any = first(
|
||||||
|
await sql`SELECT *
|
||||||
|
FROM users
|
||||||
|
WHERE id = ${id.raw}
|
||||||
|
LIMIT 1`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAdmin && !(await argon2.verify(dbUser.pass_hash, oldPassword as string))) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await argon2.hash(newPassword);
|
||||||
|
await sql`UPDATE users
|
||||||
|
SET pass_hash=${passwordHash}
|
||||||
|
WHERE id = ${id.raw}`;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
22
src/routes/auth.ts
Normal file
22
src/routes/auth.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { guard, unwrapMethod } from '../utilities/guard';
|
||||||
|
import auth from '../endpoints/auth';
|
||||||
|
import { OkResponse } from '../utilities/responseHelper';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'/api/auth/login': {
|
||||||
|
POST: unwrapMethod(auth.login),
|
||||||
|
},
|
||||||
|
'/api/auth/token': {
|
||||||
|
GET: unwrapMethod(auth.token),
|
||||||
|
},
|
||||||
|
'/api/auth/logout': {
|
||||||
|
POST: unwrapMethod(auth.logout),
|
||||||
|
},
|
||||||
|
'/api/auth/changePassword/:id': {
|
||||||
|
PATCH: guard(auth.changePassword, [Claims.ADMIN, Claims.USERS.SELF.UPDATE]),
|
||||||
|
},
|
||||||
|
'/api/auth/test': {
|
||||||
|
GET: () => new OkResponse(),
|
||||||
|
},
|
||||||
|
};
|
||||||
17
src/routes/game.ts
Normal file
17
src/routes/game.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { guard } from '../utilities/guard';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
import games from '../endpoints/games';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'/api/game': {
|
||||||
|
POST: guard(games.create, [Claims.ADMIN, Claims.GAMES.CREATE]),
|
||||||
|
},
|
||||||
|
'/api/game/:id': {
|
||||||
|
GET: guard(games.get, [Claims.ADMIN, Claims.GAMES.READ]),
|
||||||
|
PATCH: guard(games.update, [Claims.ADMIN, Claims.GAMES.UPDATE]),
|
||||||
|
DELETE: guard(games.drop, [Claims.ADMIN, Claims.GAMES.DELETE]),
|
||||||
|
},
|
||||||
|
'/api/game/search/:query': {
|
||||||
|
GET: guard(games.query, [Claims.ADMIN, Claims.GAMES.READ]),
|
||||||
|
},
|
||||||
|
};
|
||||||
14
src/routes/player.ts
Normal file
14
src/routes/player.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { guard } from '../utilities/guard';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
import player from '../endpoints/player';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'/api/player': {
|
||||||
|
POST: guard(player.create, [Claims.ADMIN, Claims.PLAYERS.CREATE]),
|
||||||
|
},
|
||||||
|
'/api/player/:id': {
|
||||||
|
GET: guard(player.get, [Claims.ADMIN, Claims.PLAYERS.OTHER.READ, Claims.PLAYERS.SELF.READ]),
|
||||||
|
PATCH: guard(player.update, [Claims.ADMIN, Claims.PLAYERS.OTHER.UPDATE, Claims.PLAYERS.SELF.UPDATE]),
|
||||||
|
DELETE: guard(player.drop, [Claims.ADMIN, Claims.PLAYERS.OTHER.DELETE, Claims.PLAYERS.SELF.DELETE]),
|
||||||
|
},
|
||||||
|
};
|
||||||
14
src/routes/user.ts
Normal file
14
src/routes/user.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { guard } from '../utilities/guard';
|
||||||
|
import user from '../endpoints/user';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'/api/user': {
|
||||||
|
POST: guard(user.create, [Claims.ADMIN, Claims.USERS.CREATE]),
|
||||||
|
},
|
||||||
|
'/api/user/:id': {
|
||||||
|
GET: guard(user.get, [Claims.ADMIN, Claims.USERS.OTHER.READ, Claims.USERS.SELF.READ]),
|
||||||
|
PATCH: guard(user.update, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]),
|
||||||
|
DELETE: guard(user.drop, [Claims.ADMIN, Claims.USERS.OTHER.UPDATE, Claims.USERS.SELF.UPDATE]),
|
||||||
|
},
|
||||||
|
};
|
||||||
119
src/tests/auth.test.ts
Normal file
119
src/tests/auth.test.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { expect, test } from 'bun:test';
|
||||||
|
import auth from '../endpoints/auth';
|
||||||
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
|
import { Claims } from '../orm/claims';
|
||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { User } from '../orm/user';
|
||||||
|
|
||||||
|
test('login', async () => {
|
||||||
|
await orm.users.create('authTest', 'test123');
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
json: {
|
||||||
|
username: 'authTest',
|
||||||
|
password: 'test123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await auth.login(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("login user that doesn't exist", async () => {
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
json: {
|
||||||
|
username: 'thisUserDoesNotExist',
|
||||||
|
password: 'test123',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await auth.login(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('login with invalid password', async () => {
|
||||||
|
const createdUser = (await orm.users.create('authTest2', 'test123')) as User;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
json: {
|
||||||
|
username: 'authTest2',
|
||||||
|
password: 'wrongPassword',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await auth.login(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change password', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.claims.push(Claims.USERS.SELF.UPDATE);
|
||||||
|
|
||||||
|
const testUser = (await orm.users.create('authTest3', 'test123')) as User;
|
||||||
|
claims.userId = testUser.id;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
params: {
|
||||||
|
id: testUser.id,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
oldPassword: 'test123',
|
||||||
|
newPassword: 'test1234',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await auth.changePassword(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change password with incorrect old password', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.claims.push(Claims.USERS.SELF.UPDATE);
|
||||||
|
|
||||||
|
const testUser = (await orm.users.create('authTest4', 'test123')) as User;
|
||||||
|
claims.userId = testUser.id;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
params: {
|
||||||
|
id: testUser.id,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
oldPassword: 'wrongPassword',
|
||||||
|
newPassword: 'test1234',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await auth.changePassword(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Change password as admin', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.userId = '1';
|
||||||
|
claims.claims.push(Claims.ADMIN);
|
||||||
|
|
||||||
|
const testUser = (await orm.users.create('authTest5', 'test123')) as User;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
params: {
|
||||||
|
id: testUser.id,
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
newPassword: 'test1234',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await auth.changePassword(request);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toBeNull();
|
||||||
|
});
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
import { expect, test, beforeAll } from 'bun:test';
|
import { expect, test, beforeAll } from 'bun:test';
|
||||||
import {sql} from "bun";
|
import { sql } from 'bun';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { beforeAll } from 'bun:test';
|
import { beforeAll } from 'bun:test';
|
||||||
import Bun from 'bun';
|
import Bun from 'bun';
|
||||||
import {sql} from "bun";
|
import { sql } from 'bun';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
console.log(process.env.DATABASE_URL);
|
console.log(process.env.DATABASE_URL);
|
||||||
@@ -24,12 +24,3 @@ beforeAll(async () => {
|
|||||||
await sql`INSERT INTO claims(name, is_default) VALUES ('USERS_OTHER_READ', true)`;
|
await sql`INSERT INTO claims(name, is_default) VALUES ('USERS_OTHER_READ', true)`;
|
||||||
await sql`INSERT INTO claims(name, is_default) VALUES ('USERS_SELF_DELETE', false)`;
|
await sql`INSERT INTO claims(name, is_default) VALUES ('USERS_SELF_DELETE', false)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { expect, test } from 'bun:test';
|
import { expect, test } from 'bun:test';
|
||||||
import user from '../endpoints/user';
|
import user from '../endpoints/user';
|
||||||
import {UnwrappedRequest} from "../utilities/guard";
|
import { UnwrappedRequest } from '../utilities/guard';
|
||||||
import {Claims} from "../orm/claims";
|
import { Claims } from '../orm/claims';
|
||||||
|
import { orm } from '../orm/orm';
|
||||||
|
import { User } from '../orm/user';
|
||||||
|
|
||||||
test('Create user as admin', async () => {
|
test('Create user as admin', async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.claims.push('ADMIN');
|
claims.claims.push(Claims.ADMIN);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
@@ -24,7 +26,7 @@ test('Create user as admin', async () => {
|
|||||||
|
|
||||||
test('Create user without read access', async () => {
|
test('Create user without read access', async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.claims.push('USERS_CREATE');
|
claims.claims.push(Claims.USERS.CREATE);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
@@ -43,7 +45,7 @@ test('Create user without read access', async () => {
|
|||||||
|
|
||||||
test('Create user that already exists', async () => {
|
test('Create user that already exists', async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.claims.push('USERS_CREATE');
|
claims.claims.push(Claims.USERS.CREATE);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
@@ -61,13 +63,13 @@ test('Create user that already exists', async () => {
|
|||||||
|
|
||||||
test('Get user', async () => {
|
test('Get user', async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.claims.push('USERS_OTHER_READ');
|
claims.claims.push(Claims.USERS.OTHER.READ);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
request: null,
|
request: null,
|
||||||
params: {
|
params: {
|
||||||
id: 1
|
id: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -79,14 +81,14 @@ test('Get user', async () => {
|
|||||||
|
|
||||||
test('Get user self with only self read permission', async () => {
|
test('Get user self with only self read permission', async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.userId = "1";
|
claims.userId = '1';
|
||||||
claims.claims.push('USERS_OTHER_READ');
|
claims.claims.push(Claims.USERS.OTHER.READ);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
request: null,
|
request: null,
|
||||||
params: {
|
params: {
|
||||||
id: 1
|
id: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,14 +100,14 @@ test('Get user self with only self read permission', async () => {
|
|||||||
|
|
||||||
test('Get other user without read permissions', async () => {
|
test('Get other user without read permissions', async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.userId = "2";
|
claims.userId = '2';
|
||||||
claims.claims.push('USERS_SELF_READ');
|
claims.claims.push(Claims.USERS.SELF.READ);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
request: null,
|
request: null,
|
||||||
params: {
|
params: {
|
||||||
id: 1
|
id: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,18 +115,170 @@ test('Get other user without read permissions', async () => {
|
|||||||
expect(response.status).toBe(401);
|
expect(response.status).toBe(401);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Get user that doesn\'t exist', async () => {
|
test("Get user that doesn't exist", async () => {
|
||||||
const claims = new Claims();
|
const claims = new Claims();
|
||||||
claims.claims.push('ADMIN');
|
claims.claims.push(Claims.ADMIN);
|
||||||
|
|
||||||
const request = new UnwrappedRequest({
|
const request = new UnwrappedRequest({
|
||||||
claims,
|
claims,
|
||||||
request: null,
|
request: null,
|
||||||
params: {
|
params: {
|
||||||
id: 101
|
id: 101,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await user.get(request);
|
const response = await user.get(request);
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Update user', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.claims.push(Claims.ADMIN);
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
json: {
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.update(request);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Update user without read access', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.userId = '1';
|
||||||
|
claims.claims.push(Claims.USERS.OTHER.UPDATE);
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
json: {
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.update(request);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Update user without permissions', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.userId = '1';
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
json: {
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.update(request);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Update user that doesn't exist", async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.userId = '1';
|
||||||
|
claims.claims.push(Claims.ADMIN);
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
json: {
|
||||||
|
isAdmin: true,
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
id: 101,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.update(request);
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete user', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.claims.push(Claims.ADMIN);
|
||||||
|
|
||||||
|
const createdUser = (await orm.users.create('test3', 'test123')) as User;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
params: {
|
||||||
|
id: createdUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.drop(request);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete user without delete permissions', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
const createdUser = (await orm.users.create('test4', 'test123')) as User;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
params: {
|
||||||
|
id: createdUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.drop(request);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete self user with only self delete permissions', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.claims.push(Claims.USERS.SELF.DELETE);
|
||||||
|
|
||||||
|
const createdUser = (await orm.users.create('test5', 'test123')) as User;
|
||||||
|
claims.userId = createdUser.id;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
params: {
|
||||||
|
id: createdUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.drop(request);
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete other user with only self delete permissions', async () => {
|
||||||
|
const claims = new Claims();
|
||||||
|
claims.userId = '1';
|
||||||
|
claims.claims.push(Claims.USERS.SELF.DELETE);
|
||||||
|
|
||||||
|
const createdUser = (await orm.users.create('test6', 'test123')) as User;
|
||||||
|
|
||||||
|
const request = new UnwrappedRequest({
|
||||||
|
claims,
|
||||||
|
request: null,
|
||||||
|
params: {
|
||||||
|
id: createdUser.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await user.drop(request);
|
||||||
|
expect(response.status).toBe(401);
|
||||||
|
});
|
||||||
|
|||||||
131
src/utilities/claimDefinitions.ts
Normal file
131
src/utilities/claimDefinitions.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
export class ClaimDefinition {
|
||||||
|
public static readonly ADMIN = 'ADMIN';
|
||||||
|
public static readonly USERS = {
|
||||||
|
CREATE: 'USERS_CREATE',
|
||||||
|
SELF: {
|
||||||
|
READ: 'USERS_SELF_READ',
|
||||||
|
UPDATE: 'USERS_SELF_UPDATE',
|
||||||
|
DELETE: 'USERS_SELF_DELETE',
|
||||||
|
},
|
||||||
|
OTHER: {
|
||||||
|
READ: 'USERS_OTHER_READ',
|
||||||
|
UPDATE: 'USERS_OTHER_UPDATE',
|
||||||
|
DELETE: 'USERS_OTHER_DELETE',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
public static readonly PLAYERS = {
|
||||||
|
CREATE: 'PLAYERS_CREATE',
|
||||||
|
SELF: {
|
||||||
|
READ: 'PLAYERS_SELF_READ',
|
||||||
|
UPDATE: 'PLAYERS_SELF_UPDATE',
|
||||||
|
DELETE: 'PLAYERS_SELF_DELETE',
|
||||||
|
},
|
||||||
|
OTHER: {
|
||||||
|
READ: 'PLAYERS_OTHER_READ',
|
||||||
|
UPDATE: 'PLAYERS_OTHER_UPDATE',
|
||||||
|
DELETE: 'PLAYERS_OTHER_DELETE',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
public static readonly CIRCLES = {
|
||||||
|
PUBLIC: {
|
||||||
|
CREATE: 'CIRCLES_PUBLIC_CREATE',
|
||||||
|
JOIN: 'CIRCLES_PUBLIC_JOIN',
|
||||||
|
USERS: {
|
||||||
|
ADD: 'CIRCLES_PUBLIC_USER_ADD',
|
||||||
|
LIST: 'CIRCLES_PUBLIC_USER_LIST',
|
||||||
|
INVITE: 'CIRCLES_PUBLIC_USER_INVITE',
|
||||||
|
},
|
||||||
|
COMMENTS: {
|
||||||
|
ADD: 'CIRCLES_PUBLIC_COMMENTS_ADD',
|
||||||
|
DELETE: 'CIRCLES_PUBLIC_COMMENTS_DELETE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PRIVATE: {
|
||||||
|
CREATE: 'CIRCLES_PRIVATE_CREATE',
|
||||||
|
USERS: {
|
||||||
|
INVITE: 'CIRCLES_PRIVATE_USER_INVITE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OWNED: {
|
||||||
|
READ: 'CIRCLES_OWNED_READ',
|
||||||
|
UPDATE: 'CIRCLES_OWNED_UPDATE',
|
||||||
|
DELETE: 'CIRCLES_OWNED_DELETE',
|
||||||
|
USERS: {
|
||||||
|
ADD: 'CIRCLES_OWNED_USER_ADD',
|
||||||
|
LIST: 'CIRCLES_OWNED_USER_LIST',
|
||||||
|
USERS: {
|
||||||
|
INVITE: 'CIRCLES_OWNED_USER_INVITE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
COMMENTS: {
|
||||||
|
ADD: 'CIRCLES_OWNED_COMMENTS_ADD',
|
||||||
|
DELETE: 'CIRCLES_OWNED_COMMENTS_DELETE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UNOWNED: {
|
||||||
|
READ: 'CIRCLES_UNOWNED_READ',
|
||||||
|
UPDATE: 'CIRCLES_UNOWNED_UPDATE',
|
||||||
|
DELETE: 'CIRCLES_UNOWNED_DELETE',
|
||||||
|
COMMENTS: {
|
||||||
|
ADD: 'CIRCLES_UNOWNED_COMMENTS_ADD',
|
||||||
|
DELETE: 'CIRCLES_UNOWNED_COMMENTS_DELETE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
public static readonly GAMES = {
|
||||||
|
CREATE: 'GAMES_CREATE',
|
||||||
|
READ: 'GAMES_READ',
|
||||||
|
UPDATE: 'GAMES_UPDATE',
|
||||||
|
DELETE: 'GAMES_DELETE',
|
||||||
|
MANAGE_IMAGES: 'GAMES_IMAGES_MANAGE',
|
||||||
|
};
|
||||||
|
public static readonly MATCHES = {
|
||||||
|
CREATE: 'MATCHES_CREATE',
|
||||||
|
OWNED: {
|
||||||
|
READ: 'MATCHES_OWNED_READ',
|
||||||
|
UPDATE: 'MATCHES_OWNED_UPDATE',
|
||||||
|
DELETE: 'MATCHES_OWNED_DELETE',
|
||||||
|
COMMENTS: {
|
||||||
|
ADD: 'MATCHES_OWNED_COMMENTS_ADD',
|
||||||
|
DELETE: 'MATCHES_OWNED_COMMENTS_DELETE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UNOWNED: {
|
||||||
|
READ: 'MATCHES_UNOWNED_READ',
|
||||||
|
UPDATE: 'MATCHES_UNOWNED_UPDATE',
|
||||||
|
DELETE: 'MATCHES_UNOWNED_DELETE',
|
||||||
|
COMMENTS: {
|
||||||
|
ADD: 'MATCHES_UNOWNED_COMMENTS_ADD',
|
||||||
|
DELETE: 'MATCHES_UNOWNED_COMMENTS_DELETE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
public static readonly COLLECTIONS = {
|
||||||
|
CREATE: 'COLLECTIONS_CREATE',
|
||||||
|
OWNED: {
|
||||||
|
READ: 'COLLECTIONS_OWNED_READ',
|
||||||
|
UPDATE: 'COLLECTIONS_OWNED_UPDATE',
|
||||||
|
DELETE: 'COLLECTIONS_OWNED_DELETE',
|
||||||
|
COMMENTS: {
|
||||||
|
DELETE: 'COLLECTIONS_OWNED_COMMENTS_DELETE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UNOWNED: {
|
||||||
|
READ: 'COLLECTIONS_UNOWNED_READ',
|
||||||
|
UPDATE: 'COLLECTIONS_UNOWNED_UPDATE',
|
||||||
|
DELETE: 'COLLECTIONS_UNOWNED_DELETE',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
public static readonly COMMENTS = {
|
||||||
|
OWNED: {
|
||||||
|
READ: 'COMMENTS_OWNED_READ',
|
||||||
|
UPDATE: 'COMMENTS_OWNED_UPDATE',
|
||||||
|
DELETE: 'COMMENTS_OWNED_DELETE',
|
||||||
|
},
|
||||||
|
UNOWNED: {
|
||||||
|
READ: 'COMMENTS_UNOWNED_READ',
|
||||||
|
UPDATE: 'COMMENTS_UNOWNED_UPDATE',
|
||||||
|
DELETE: 'COMMENTS_UNOWNED_DELETE',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/utilities/elo.ts
Normal file
45
src/utilities/elo.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { orderBy } from 'lodash';
|
||||||
|
|
||||||
|
interface GamePlayer {
|
||||||
|
id: string;
|
||||||
|
elo: number;
|
||||||
|
gamesPlayed: number;
|
||||||
|
standing: number;
|
||||||
|
eloChange?: number;
|
||||||
|
}
|
||||||
|
export interface GamePlayed {
|
||||||
|
players: GamePlayer[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateElos(game: GamePlayed, provisionalPeriod: number = 1): GamePlayed {
|
||||||
|
const orderedResults = orderBy(game.players, 'standing', 'asc');
|
||||||
|
for (let i = 0; i < orderedResults.length - 1; i++) {
|
||||||
|
for (let j = i + 1; j < orderedResults.length; j++) {
|
||||||
|
const challengerResults = calculateEloChange(orderedResults[i].elo, orderedResults[j].elo);
|
||||||
|
orderedResults[i].eloChange =
|
||||||
|
(orderedResults[i].eloChange ?? 0) +
|
||||||
|
challengerResults.winnerChange * Math.min(1, orderedResults[j].gamesPlayed / provisionalPeriod);
|
||||||
|
orderedResults[j].eloChange =
|
||||||
|
(orderedResults[j].eloChange ?? 0) +
|
||||||
|
challengerResults.loserChange * Math.min(1, orderedResults[i].gamesPlayed / provisionalPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
players: orderedResults,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EloResult {
|
||||||
|
winnerChange: number;
|
||||||
|
loserChange: number;
|
||||||
|
}
|
||||||
|
function calculateEloChange(winnerElo: number, loserElo: number, draw: boolean = false): EloResult {
|
||||||
|
const ratingStep = 32;
|
||||||
|
const expectedWinnerResult = 1 / (1 + Math.pow(10, (loserElo - winnerElo) / 400));
|
||||||
|
const expectedLoserResult = 1 / (1 + Math.pow(10, (winnerElo - loserElo) / 400));
|
||||||
|
|
||||||
|
return {
|
||||||
|
winnerChange: ratingStep * ((draw ? 0.5 : 1) - expectedWinnerResult),
|
||||||
|
loserChange: ratingStep * ((draw ? 0.5 : 0) - expectedLoserResult),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
export class BadRequestError extends Error {
|
export class BadRequestError extends Error {
|
||||||
constructor(message: string | undefined = undefined) {
|
constructor(message?: string | undefined) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
export class UnauthorizedError extends Error {
|
||||||
constructor(message: string | undefined = undefined) {
|
constructor(message?: string | undefined) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NotFoundError extends Error {
|
export class NotFoundError extends Error {
|
||||||
constructor(message: string | undefined = undefined) {
|
constructor(message?: string | undefined) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { BunRequest as Request } from 'bun';
|
import { BunRequest as Request } from 'bun';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt, { TokenExpiredError } from 'jsonwebtoken';
|
||||||
import {ErrorResponse} from "./responseHelper";
|
import { ErrorResponse, UnauthorizedResponse } from './responseHelper';
|
||||||
import {UnauthorizedError} from "./errors";
|
import { UnauthorizedError } from './errors';
|
||||||
import {Claims} from "../orm/claims";
|
import { Claims } from '../orm/claims';
|
||||||
|
import HashIds from 'hashids';
|
||||||
|
|
||||||
export function guardRedirect(method: Function, redirectMethod: Function, guardedClaims: string[] | undefined = undefined) {
|
export const hashIds = new HashIds(process.env.JWT_SECRET, 4);
|
||||||
|
|
||||||
|
export function guardRedirect(
|
||||||
|
method: (request: UnwrappedRequest<any>) => Promise<Response> | Response,
|
||||||
|
redirectMethod: Function,
|
||||||
|
guardedClaims: string[],
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
return guard(method, guardedClaims);
|
return guard(method, guardedClaims);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -12,47 +19,56 @@ export function guardRedirect(method: Function, redirectMethod: Function, guarde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function guard(method: Function, guardedClaims: string[] | undefined = undefined): (r: Request) => Promise<Response> {
|
export function guard(
|
||||||
|
method: (request: UnwrappedRequest<any>) => Promise<Response> | Response,
|
||||||
|
guardedClaims: string[],
|
||||||
|
): (r: Request) => Promise<Response> {
|
||||||
return async (request: Request): Promise<Response> => {
|
return async (request: Request): Promise<Response> => {
|
||||||
const authHeader: string | null = request.headers.get('Authorization')?.replace(/^Bearer /, '') as string ?? null;
|
const authHeader: string | null =
|
||||||
|
(request.headers.get('Authorization')?.replace(/^Bearer /, '') as string) ?? null;
|
||||||
try {
|
try {
|
||||||
const userClaims: Claims = jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as Claims;
|
const userClaims: Claims = jwt.verify(authHeader as string, process.env.JWT_SECRET_KEY as string) as Claims;
|
||||||
if (guardedClaims !== undefined && !userClaims.claims.some((x: string): boolean => guardedClaims.includes(x))) {
|
if (!userClaims.claims.some((x: string): boolean => guardedClaims.includes(x))) {
|
||||||
throw new UnauthorizedError('Unauthorized');
|
throw new UnauthorizedError('Unauthorized');
|
||||||
}
|
}
|
||||||
return method(await unwrap(request, userClaims));
|
return method(await unwrap(request, userClaims));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
if (error instanceof TokenExpiredError) {
|
||||||
|
return new UnauthorizedResponse(error.message);
|
||||||
|
}
|
||||||
return new ErrorResponse(error as Error);
|
return new ErrorResponse(error as Error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnwrappedRequest {
|
export class UnwrappedRequest<T> {
|
||||||
readonly json: any;
|
readonly body: T;
|
||||||
readonly request: Request;
|
readonly request: Request;
|
||||||
readonly params: { [x: string]: string };
|
readonly params: { [x: string]: string };
|
||||||
readonly claims: Claims;
|
readonly claims: Claims;
|
||||||
|
|
||||||
constructor(input: any) {
|
constructor(input: any) {
|
||||||
this.json = input.json;
|
this.body = input.body;
|
||||||
this.request = input.request;
|
this.request = input.request;
|
||||||
this.claims = input.claims;
|
this.claims = input.claims || new Claims();
|
||||||
this.params = input.params;
|
this.params = input.params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unwrap(request: Request, claims: Claims | null = null) {
|
export async function unwrap<T>(request: Request, claims?: Claims) {
|
||||||
return new UnwrappedRequest({
|
return new UnwrappedRequest<T>({
|
||||||
request,
|
request,
|
||||||
claims,
|
claims,
|
||||||
json: request.body ? await request.json() : null,
|
body: request.body ? await request.json() : null,
|
||||||
params: request.params,
|
params: request.params,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapMethod(methodToUnwrap: ((r: UnwrappedRequest) => Response) | ((r: UnwrappedRequest) => Promise<Response>)): (r: Request) => Promise<Response> {
|
export function unwrapMethod<T>(
|
||||||
|
methodToUnwrap: ((r: UnwrappedRequest<T>) => Response) | ((r: UnwrappedRequest<T>) => Promise<Response>),
|
||||||
|
): (r: Request) => Promise<Response> {
|
||||||
return async (request: Request) => {
|
return async (request: Request) => {
|
||||||
const unwrappedRequest = await unwrap(request);
|
const unwrappedRequest = await unwrap<T>(request);
|
||||||
return await methodToUnwrap(unwrappedRequest);
|
return await methodToUnwrap(unwrappedRequest);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
20
src/utilities/helpers.ts
Normal file
20
src/utilities/helpers.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export function memo<T extends (...args: any[]) => {}, S>(
|
||||||
|
func: T,
|
||||||
|
lifespan: number = 5 * 60 * 1000,
|
||||||
|
keyDelegate?: (...args: any[]) => string,
|
||||||
|
): T {
|
||||||
|
const cache: { [key: string]: { value: S; timestamp: number } } = {};
|
||||||
|
return ((...args: any[]): S => {
|
||||||
|
const key: string = (keyDelegate ? keyDelegate(...args) : args?.[0]?.toString()) ?? '';
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (!cache[key] || now - cache[key].timestamp > lifespan) {
|
||||||
|
cache[key] = {
|
||||||
|
value: func(...args) as S,
|
||||||
|
timestamp: now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache[key].value;
|
||||||
|
}) as unknown as T;
|
||||||
|
}
|
||||||
75
src/utilities/requestModels.ts
Normal file
75
src/utilities/requestModels.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { hashIds } from './guard';
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
export interface ChangePasswordRequest {
|
||||||
|
oldPassword: string | null;
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
|
export interface CreateUserRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
playerId: string;
|
||||||
|
}
|
||||||
|
export interface UpdateUserRequest {
|
||||||
|
isActive?: boolean;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}
|
||||||
|
export interface CreatePlayerRequest {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
export interface UpdatePlayerRequest {
|
||||||
|
name?: string;
|
||||||
|
isRatingLocked?:boolean;
|
||||||
|
canBeMultiple?:boolean;
|
||||||
|
}
|
||||||
|
export interface CreateGameRequest {
|
||||||
|
name: string;
|
||||||
|
imagePath?: string;
|
||||||
|
bggId?: string;
|
||||||
|
}
|
||||||
|
export interface UpdateGameRequest {
|
||||||
|
name: string;
|
||||||
|
imagePath?: string;
|
||||||
|
bggId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecureId {
|
||||||
|
#hashedValue?: string;
|
||||||
|
#secureValue?: string;
|
||||||
|
get value(): string | undefined {
|
||||||
|
return this.#hashedValue;
|
||||||
|
}
|
||||||
|
set value(value: string) {
|
||||||
|
this.#hashedValue = value;
|
||||||
|
this.#secureValue = hashIds.decode(value)?.toString();
|
||||||
|
}
|
||||||
|
get raw(): string | undefined {
|
||||||
|
return this.#secureValue;
|
||||||
|
}
|
||||||
|
set raw(value: string) {
|
||||||
|
this.#hashedValue = hashIds.encode(value);
|
||||||
|
this.#secureValue = value;
|
||||||
|
}
|
||||||
|
constructor(id: { public?: string; secure?: string }) {
|
||||||
|
if (id.public) {
|
||||||
|
this.value = id.public;
|
||||||
|
} else if (id.secure) {
|
||||||
|
this.raw = id.secure;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): string | undefined {
|
||||||
|
return this.#hashedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromHash(hash: string) {
|
||||||
|
return new SecureId({ public: hash });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromID(id: string) {
|
||||||
|
return new SecureId({ secure: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,75 @@
|
|||||||
import {BadRequestError, NotFoundError, UnauthorizedError} from "./errors";
|
import { BadRequestError, NotFoundError, UnauthorizedError } from './errors';
|
||||||
|
import { hashIds } from './guard';
|
||||||
|
|
||||||
export class ErrorResponse extends Response {
|
export class ErrorResponse extends Response {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
constructor(error: Error) {
|
constructor(error: Error) {
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
return Response.json({message: error.message}, {status: 400});
|
return new BadRequestResponse(error.message);
|
||||||
}
|
} else if (error instanceof UnauthorizedError) {
|
||||||
else if(error instanceof UnauthorizedError){
|
return new UnauthorizedResponse(error.message);
|
||||||
return Response.json({message: error.message}, {status: 401});
|
} else if (error instanceof NotFoundError) {
|
||||||
}
|
return new NotFoundResponse(error.message);
|
||||||
else if(error instanceof NotFoundError){
|
|
||||||
return Response.json({message: error.message}, {status: 404});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({ message: error.message }, { status: 500 });
|
return Response.json({ message: error.message }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class BadRequestResponse extends Response {
|
||||||
|
// @ts-ignore
|
||||||
|
constructor(message?: string) {
|
||||||
|
return Response.json({ message: message }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedResponse extends Response {
|
||||||
|
// @ts-ignore
|
||||||
|
constructor(message?: string) {
|
||||||
|
return Response.json({ message: message }, { status: 401 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundResponse extends Response {
|
||||||
|
// @ts-ignore
|
||||||
|
constructor(message?: string) {
|
||||||
|
return Response.json({ message: message }, { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OkResponse extends Response {
|
||||||
|
// @ts-ignore
|
||||||
|
constructor(body?: Model | null) {
|
||||||
|
if (body) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
...body,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreatedResponse extends Response {
|
||||||
|
// @ts-ignore
|
||||||
|
constructor(body?: any) {
|
||||||
|
if (body) {
|
||||||
|
return Response.json({ ...body }, { status: 201 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 201 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user