diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..92a1493 --- /dev/null +++ b/TODO.md @@ -0,0 +1,3 @@ +- [ ] An audit log +- [ ] Actual invite code pruning +- [ ] Make the journal work (i dont really remember what was left out though) diff --git a/db/schema.ts b/db/schema.ts index 5ad2c2f..cab6909 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -29,7 +29,9 @@ export const updates = pgTable("updates", { export const journalEntries = pgTable("journal_entries", { id: integer().primaryKey().generatedAlwaysAsIdentity(), - user: integer().references(() => users.id, { onDelete: "cascade" }).notNull(), + user: integer() + .references(() => users.id, { onDelete: "cascade" }) + .notNull(), moodChange: integer("mood-change").default(0).notNull(), entry: varchar({ length: 4096 }).default("").notNull(), visibility: integer().default(1).notNull(), @@ -61,3 +63,11 @@ export const follows = pgTable( }; } ); + +export const inviteCodes = pgTable("invite_codes", { + token: varchar({ length: 22 }).primaryKey(), + user: integer("user_id").references(() => users.id, { onDelete: "cascade" }), + granted: timestamp().notNull(), + expires: timestamp().default(new Date(0)), + confersModerator: boolean("confers_moderator").default(false) +}); diff --git a/drizzle/0007_silly_freak.sql b/drizzle/0007_silly_freak.sql new file mode 100644 index 0000000..29dfbff --- /dev/null +++ b/drizzle/0007_silly_freak.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS "invite_codes" ( + "token" varchar(8) PRIMARY KEY NOT NULL, + "user_id" integer, + "granted" timestamp NOT NULL, + "expires" timestamp DEFAULT '1970-01-01 00:00:00.000' +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "invite_codes" ADD CONSTRAINT "invite_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/0008_huge_pyro.sql b/drizzle/0008_huge_pyro.sql new file mode 100644 index 0000000..63dab1d --- /dev/null +++ b/drizzle/0008_huge_pyro.sql @@ -0,0 +1,2 @@ +ALTER TABLE "invite_codes" ALTER COLUMN "token" SET DATA TYPE varchar(20);--> statement-breakpoint +ALTER TABLE "invite_codes" ADD COLUMN "confersAdmin" boolean DEFAULT false; \ No newline at end of file diff --git a/drizzle/0009_unusual_ozymandias.sql b/drizzle/0009_unusual_ozymandias.sql new file mode 100644 index 0000000..5514790 --- /dev/null +++ b/drizzle/0009_unusual_ozymandias.sql @@ -0,0 +1 @@ +ALTER TABLE "invite_codes" RENAME COLUMN "confersAdmin" TO "confers_admin"; \ No newline at end of file diff --git a/drizzle/0010_exotic_ted_forrester.sql b/drizzle/0010_exotic_ted_forrester.sql new file mode 100644 index 0000000..353a3a2 --- /dev/null +++ b/drizzle/0010_exotic_ted_forrester.sql @@ -0,0 +1 @@ +ALTER TABLE "invite_codes" RENAME COLUMN "confers_admin" TO "confers_moderator"; \ No newline at end of file diff --git a/drizzle/0011_loose_dreadnoughts.sql b/drizzle/0011_loose_dreadnoughts.sql new file mode 100644 index 0000000..203c450 --- /dev/null +++ b/drizzle/0011_loose_dreadnoughts.sql @@ -0,0 +1 @@ +ALTER TABLE "invite_codes" ALTER COLUMN "token" SET DATA TYPE varchar(22); \ No newline at end of file diff --git a/drizzle/meta/0007_snapshot.json b/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..148389c --- /dev/null +++ b/drizzle/meta/0007_snapshot.json @@ -0,0 +1,408 @@ +{ + "id": "f40891bb-8a67-4d29-8772-b05615e0aa9f", + "prevId": "73a239ee-a945-48e0-a021-d415815a830b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(8)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "moderator": { + "name": "moderator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0008_snapshot.json b/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..ae0511b --- /dev/null +++ b/drizzle/meta/0008_snapshot.json @@ -0,0 +1,415 @@ +{ + "id": "09a82ff0-6a18-4587-a33f-a55c2fef2ebf", + "prevId": "f40891bb-8a67-4d29-8772-b05615e0aa9f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(20)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + }, + "confersAdmin": { + "name": "confersAdmin", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "moderator": { + "name": "moderator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..3cb2309 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,415 @@ +{ + "id": "49ea990a-0039-44d5-a35e-319db2299a46", + "prevId": "09a82ff0-6a18-4587-a33f-a55c2fef2ebf", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(20)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + }, + "confers_admin": { + "name": "confers_admin", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "moderator": { + "name": "moderator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0010_snapshot.json b/drizzle/meta/0010_snapshot.json new file mode 100644 index 0000000..1663230 --- /dev/null +++ b/drizzle/meta/0010_snapshot.json @@ -0,0 +1,415 @@ +{ + "id": "61539639-4e1f-4bf2-854e-1657c5024c6c", + "prevId": "49ea990a-0039-44d5-a35e-319db2299a46", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(20)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + }, + "confers_moderator": { + "name": "confers_moderator", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "moderator": { + "name": "moderator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0011_snapshot.json b/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..65dbc70 --- /dev/null +++ b/drizzle/meta/0011_snapshot.json @@ -0,0 +1,415 @@ +{ + "id": "d928a4c4-ab43-4979-b6aa-3892f29701c8", + "prevId": "61539639-4e1f-4bf2-854e-1657c5024c6c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.follows": { + "name": "follows", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "follower_id": { + "name": "follower_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "follows_user_id_users_id_fk": { + "name": "follows_user_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "follows_follower_id_users_id_fk": { + "name": "follows_follower_id_users_id_fk", + "tableFrom": "follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "follows_user_id_follower_id_pk": { + "name": "follows_user_id_follower_id_pk", + "columns": [ + "user_id", + "follower_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invite_codes": { + "name": "invite_codes", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar(22)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "granted": { + "name": "granted", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "'1970-01-01 00:00:00.000'" + }, + "confers_moderator": { + "name": "confers_moderator", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "invite_codes_user_id_users_id_fk": { + "name": "invite_codes_user_id_users_id_fk", + "tableFrom": "invite_codes", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "journal_entries_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood-change": { + "name": "mood-change", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "entry": { + "name": "entry", + "type": "varchar(4096)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visibility": { + "name": "visibility", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "journal_entries_user_users_id_fk": { + "name": "journal_entries_user_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profiles": { + "name": "profiles", + "schema": "", + "columns": { + "user": { + "name": "user", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "bio": { + "name": "bio", + "type": "varchar(1024)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "website": { + "name": "website", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": { + "profiles_user_users_id_fk": { + "name": "profiles_user_users_id_fk", + "tableFrom": "profiles", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.updates": { + "name": "updates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "updates_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user": { + "name": "user", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mood": { + "name": "mood", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "description": { + "name": "description", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "date": { + "name": "date", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "updates_user_users_id_fk": { + "name": "updates_user_users_id_fk", + "tableFrom": "updates", + "tableTo": "users", + "columnsFrom": [ + "user" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(26)", + "primaryKey": false, + "notNull": true + }, + "pass": { + "name": "pass", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "registered": { + "name": "registered", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "moderator": { + "name": "moderator", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 198d197..75d0331 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -50,6 +50,41 @@ "when": 1731868543611, "tag": "0006_lame_grey_gargoyle", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1733871933739, + "tag": "0007_silly_freak", + "breakpoints": true + }, + { + "idx": 8, + "version": "7", + "when": 1733873221416, + "tag": "0008_huge_pyro", + "breakpoints": true + }, + { + "idx": 9, + "version": "7", + "when": 1733873440944, + "tag": "0009_unusual_ozymandias", + "breakpoints": true + }, + { + "idx": 10, + "version": "7", + "when": 1733874003964, + "tag": "0010_exotic_ted_forrester", + "breakpoints": true + }, + { + "idx": 11, + "version": "7", + "when": 1733874202840, + "tag": "0011_loose_dreadnoughts", + "breakpoints": true } ] } \ No newline at end of file diff --git a/main.ts b/main.ts index fff7e36..1579478 100644 --- a/main.ts +++ b/main.ts @@ -5,21 +5,22 @@ import "dotenv/config"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime.js"; -import express from "express"; +import express, { Express } from "express"; import bodyParser from "body-parser"; import session from "express-session"; import connectPgSimple from "connect-pg-simple"; import flash from "connect-flash"; -import { drizzle } from "drizzle-orm/node-postgres"; +import { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres"; import { updates, users } from "./db/schema.js"; -import { desc, eq } from "drizzle-orm"; +import { count, desc, eq } from "drizzle-orm"; // routes +import adminRoutes from "./routes/admin.js"; import loginRoutes from "./routes/login.js"; import userRoutes from "./routes/users.js"; import updateRoutes from "./routes/updates.js"; -import { getMoods, render, setNonce } from "./routes/util.js"; +import { createInviteCode, getMoods, render, setNonce } from "./routes/util.js"; const db = drizzle(process.env.DATABASE_URL!); @@ -58,6 +59,8 @@ object-src 'none'; base-uri 'none';" return next(); }); + await init(app, db); + app.get("/", async (req, res) => { const upd = db .selectDistinctOn([updates.user], { @@ -100,6 +103,7 @@ object-src 'none'; base-uri 'none';" }); }); + adminRoutes(app, db); await userRoutes(app, db); await updateRoutes(app, db); loginRoutes(app, db); @@ -108,3 +112,12 @@ object-src 'none'; base-uri 'none';" console.log("Listening on http://127.0.0.1:1337/"); }); })(); + +async function init(app: Express, db: NodePgDatabase) { + const totalUsers = await db.select({ value: count() }).from(users); + if (totalUsers[0].value === 0) { + console.log("There are no users registered. Creating a temporary invite code right now."); + console.log(" " + await createInviteCode(db, 0, new Date(Date.now() + (1 * 1000 * 60)), true)); + console.log("This code expires in 1 minute and will confer admin powers to whoever signs up with it."); + } +} diff --git a/routes/admin.ts b/routes/admin.ts new file mode 100644 index 0000000..6737236 --- /dev/null +++ b/routes/admin.ts @@ -0,0 +1,60 @@ +import { NodePgDatabase } from "drizzle-orm/node-postgres"; +import { Express } from "express"; +import { createInviteCode, render } from "./util.js"; +import { inviteCodes, users } from "../db/schema.js"; +import { desc, eq } from "drizzle-orm"; +import dayjs from "dayjs"; + +export default function (app: Express, db: NodePgDatabase) { + app.get("/mod", async (req, res) => { + if (!req.session["loggedIn"] || !req.session["moderator"]) { + res.redirect("/"); + return; + } + + const now = dayjs(); + const codes = ( + await db + .select({ expires: inviteCodes.expires, token: inviteCodes.token, uname: users.name }) + .from(inviteCodes) + .leftJoin(users, eq(inviteCodes.user, users.id)) + .orderBy(desc(inviteCodes.granted)) + ).map((e) => { + return { + expires: e.expires, + token: e.token, + uname: e.uname, + expiresString: now.to(dayjs(e.expires)) + }; + }); + render(db, "admin", "Admin Panel", res, req, { codes }); + }); + + app.post("/mod/codes/delete", async (req, res) => { + if (!req.session["loggedIn"] || !req.session["moderator"]) { + res.redirect("/"); + return; + } + + await db.delete(inviteCodes).where(eq(inviteCodes.token, req.body.token)); + req.flash("success", "Deleted."); + res.redirect("/mod"); + }) + app.post("/mod/codes/create", async (req, res) => { + if (!req.session["loggedIn"] || !req.session["moderator"]) { + res.redirect("/"); + return; + } + + const expiration = new Date(req.body.expiration || 0); + if (req.body.expiration && expiration.getTime() <= Date.now()) { + req.flash("error", "Chosen expiration date is in the past."); + res.redirect("/mod"); + return; + } + const code = await createInviteCode(db, req.session["uid"], expiration); + + req.flash("success", `Your code has been created as ${code}.`); + res.redirect("/mod"); + }); +} diff --git a/routes/login.ts b/routes/login.ts index 40c4cc8..4cd9efa 100644 --- a/routes/login.ts +++ b/routes/login.ts @@ -2,7 +2,7 @@ import { Express } from "express"; import bcrypt from "bcrypt"; import { render } from "./util.js"; import { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { profiles, users } from "../db/schema.js"; +import { inviteCodes, profiles, users } from "../db/schema.js"; import { eq } from "drizzle-orm"; //! TEMP Also not sanitized like at all @@ -20,6 +20,12 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/"); return; } + // validation + if (req.body.referral.length < 22) { + req.flash("error", "Invalid invite code! Make sure you pasted it in correctly WITH the hyphens."); + res.redirect("/register"); + return; + } if (req.body.name.length < 3) { req.flash("error", "Username can't be shorter than 3 characters"); res.redirect("/register"); @@ -43,7 +49,24 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/register"); return; } + + // invite code checking + const code = (await db.select({ expires: inviteCodes.expires, confersModerator: inviteCodes.confersModerator }).from(inviteCodes).where(eq(inviteCodes.token, req.body.referral)).limit(1))[0]; + if (!code) { + req.flash("error", "Invalid invite code! Make sure you pasted it in correctly WITH the hyphens."); + res.redirect("/register"); + return; + } + const expiration = code.expires.getTime(); + if (expiration > 0 && Date.now() >= expiration) { + req.flash("error", "That code is expired."); + res.redirect("/register"); + return; + } + // we're verified now so get that dumb fucker out of the database + await db.delete(inviteCodes).where(eq(inviteCodes.token, req.body.referral)); + // field conflicts if ( (await db.select().from(users).where(eq(users.name, req.body.name))) .length > 0 @@ -66,10 +89,12 @@ export default function(app: Express, db: NodePgDatabase) { const { uid } = ( await db .insert(users) + //@ts-expect-error .values({ name: req.body.name, email: req.body.email, //! Not actually validating this like at all??? pass: hash, + moderator: code.confersModerator, registered: new Date(Date.now()) }) .returning({ uid: users.id }) @@ -77,6 +102,7 @@ export default function(app: Express, db: NodePgDatabase) { await db.insert(profiles).values({ user: uid }); req.session["loggedIn"] = true; + req.session["moderator"] = code.confersModerator; req.session["user"] = req.body.name; req.session["uid"] = uid; req.flash( @@ -106,16 +132,17 @@ export default function(app: Express, db: NodePgDatabase) { res.redirect("/login"); return; } - if (await bcrypt.compare(req.body.pass, user.pass)) { - req.session["loggedIn"] = true; - req.session["user"] = user.name; - req.session["uid"] = user.id; - req.flash("success", "You're logged in! Welcome back!!"); - res.redirect("/dashboard"); - } else { + if (!(await bcrypt.compare(req.body.pass, user.pass))) { req.flash("error", "The username or password is invalid! I'm sorry! :("); res.redirect("/login"); + return; } + req.session["loggedIn"] = true; + req.session["moderator"] = user.moderator; + req.session["user"] = user.name; + req.session["uid"] = user.id; + req.flash("success", "You're logged in! Welcome back!!"); + res.redirect("/dashboard"); }); app.get("/logout", (req, res) => { req.session["loggedIn"] = false; diff --git a/routes/util.ts b/routes/util.ts index 3edfe4b..33b7652 100644 --- a/routes/util.ts +++ b/routes/util.ts @@ -1,7 +1,7 @@ import { NodePgDatabase } from "drizzle-orm/node-postgres"; import type { Request, Response } from "express"; -import { updates } from "../db/schema.js"; -import { desc, eq } from "drizzle-orm"; +import { inviteCodes, updates } from "../db/schema.js"; +import { count, desc, eq } from "drizzle-orm"; import fs from "node:fs/promises"; const nonceChars = @@ -62,3 +62,29 @@ export async function render( }; res.render(page, { ...o, ...stuff }); } + +const inviteCodeChars = "abcdefghijklmnopqrstuvwxyz0123456789" +export async function createInviteCode(db: NodePgDatabase, user: number, expires: Date, confersModerator = false) { + let existingToken = 1, token: string; + while (existingToken) { + token = user.toString().padStart(4, "0") + "-" + for (let i = 0; i < 17; i++) { + if ((i + 1) % 6 === 0) { + token += "-"; + continue; + } + token += inviteCodeChars[Math.floor(Math.random() * inviteCodeChars.length)]; + } + existingToken = (await db.select({ value: count() }).from(inviteCodes).where(eq(inviteCodes.token, token)))[0].value; + } + + //@ts-expect-error + await db.insert(inviteCodes).values({ + token, + user: user || undefined, + granted: new Date(Date.now()), + expires, + confersModerator + }); + return token; +} diff --git a/static/css/main.css b/static/css/main.css index af0b696..781a589 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -148,6 +148,13 @@ summary { border-color: #3da758; } +.error { + color: #c51e48; +} +.subtle { + opacity: 0.5; +} + button, textarea, select, @@ -169,6 +176,14 @@ select { cursor: pointer; } +table button { + padding: 0 4px; +} +th, td { + text-align: left; + padding-right: 1em; +} + .feed-update { background-color: #d5d4bb; height: fit-content; diff --git a/views/admin.pug b/views/admin.pug new file mode 100644 index 0000000..815dfd6 --- /dev/null +++ b/views/admin.pug @@ -0,0 +1,32 @@ +extends site.pug + +block content + h1 Admin Panel + h2 Invite codes + form(action="/mod/codes/delete", method="post") + table + tbody + tr + th Token + th Expires + th Creator + for code of codes + - const timestamp = code.expires.getTime() + tr + td= code.token + if timestamp === 0 + td.subtle never + else if Date.now() >= timestamp + td.error EXPIRED + else + td= code.expiresString + td + a(href=`/users/${code.uname}`)= code.uname + td + button(type="submit", name="token", value=code.token) Delete + form(action="/mod/codes/prune", method="post") + button(type="submit") Prune + br + form(action="/mod/codes/create", method="post") + input(type="datetime-local", name="expiration") + button(type="submit") Create diff --git a/views/dashboard.pug b/views/dashboard.pug index a321dfa..d05dd32 100644 --- a/views/dashboard.pug +++ b/views/dashboard.pug @@ -36,7 +36,7 @@ block content h1(style="margin-top:0.5em;") Feed include _feed.pug +feed(recentUpdates) - script. + script(nonce=nonce). function disable(form) { const btn = form.querySelector("button"); btn.setAttribute("disabled", true); diff --git a/views/register.pug b/views/register.pug index d32a5c3..d4778c2 100644 --- a/views/register.pug +++ b/views/register.pug @@ -9,4 +9,6 @@ block content br input(type="password", name="pass", placeholder="Password", required) br + input(type="text", name="referral", placeholder="Invite Code", maxlength="22", required) + br button(type="submit") Sign Up diff --git a/views/site.pug b/views/site.pug index 82fa2ef..a8859d3 100644 --- a/views/site.pug +++ b/views/site.pug @@ -14,6 +14,8 @@ html(lang="en") img#header-logo(src="/img/logo.svg", alt="logo") nav if session.loggedIn + if session.moderator + a(href="/mod/") Admin a(href="/dashboard/") Dashboard a(href="/journal/") Journal a(href="/logout/") Log out