From 207217cf24c9723fbf64a039401b326eac9fc239 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 00:30:40 +0300 Subject: [PATCH 1/7] add api key table, add timezones to all timestamps --- drizzle/0013_charming_madame_hydra.sql | 15 + drizzle/0014_simple_psylocke.sql | 7 + drizzle/meta/0013_snapshot.json | 446 ++++++++++++++++++++ drizzle/meta/0014_snapshot.json | 446 ++++++++++++++++++++ drizzle/meta/_journal.json | 14 + src/lib/server/database/schema/analytics.ts | 2 +- src/lib/server/database/schema/api.ts | 28 ++ src/lib/server/database/schema/auth.ts | 4 +- src/lib/server/database/schema/link.ts | 6 +- 9 files changed, 962 insertions(+), 6 deletions(-) create mode 100644 drizzle/0013_charming_madame_hydra.sql create mode 100644 drizzle/0014_simple_psylocke.sql create mode 100644 drizzle/meta/0013_snapshot.json create mode 100644 drizzle/meta/0014_snapshot.json create mode 100644 src/lib/server/database/schema/api.ts diff --git a/drizzle/0013_charming_madame_hydra.sql b/drizzle/0013_charming_madame_hydra.sql new file mode 100644 index 0000000..52aa350 --- /dev/null +++ b/drizzle/0013_charming_madame_hydra.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "api_key" ( + "id" varchar(15), + "secret" text, + "user_id" varchar(15) NOT NULL, + "timestamp" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "api_key_id_secret_pk" PRIMARY KEY("id","secret") +); +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "api_key_user_id_idx" ON "api_key" ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "api_key_created_at_idx" ON "api_key" ("timestamp");--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "api_key" ADD CONSTRAINT "api_key_user_id_auth_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/0014_simple_psylocke.sql b/drizzle/0014_simple_psylocke.sql new file mode 100644 index 0000000..217aae5 --- /dev/null +++ b/drizzle/0014_simple_psylocke.sql @@ -0,0 +1,7 @@ +ALTER TABLE "link_visit" ALTER COLUMN "timestamp" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "api_key" ALTER COLUMN "timestamp" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "signup_token" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "signup_token" ALTER COLUMN "used_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "links" ALTER COLUMN "created_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "links" ALTER COLUMN "last_used" SET DATA TYPE timestamp with time zone;--> statement-breakpoint +ALTER TABLE "links" ALTER COLUMN "valid_until" SET DATA TYPE timestamp with time zone; \ No newline at end of file diff --git a/drizzle/meta/0013_snapshot.json b/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..343f7e7 --- /dev/null +++ b/drizzle/meta/0013_snapshot.json @@ -0,0 +1,446 @@ +{ + "id": "f74c9758-b6ae-4611-8007-a5e449bba755", + "prevId": "1c466eec-86bd-495c-bb9d-9fc90f7a01ef", + "version": "5", + "dialect": "pg", + "tables": { + "link_visit": { + "name": "link_visit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "link_id": { + "name": "link_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "link_visit_link_id_index": { + "name": "link_visit_link_id_index", + "columns": [ + "link_id" + ], + "isUnique": false + }, + "link_visit_timestamp_index": { + "name": "link_visit_timestamp_index", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "link_visit_link_id_links_id_fk": { + "name": "link_visit_link_id_links_id_fk", + "tableFrom": "link_visit", + "tableTo": "links", + "columnsFrom": [ + "link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_key_user_id_idx": { + "name": "api_key_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_key_created_at_idx": { + "name": "api_key_created_at_idx", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_key_user_id_auth_user_id_fk": { + "name": "api_key_user_id_auth_user_id_fk", + "tableFrom": "api_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "api_key_id_secret_pk": { + "name": "api_key_id_secret_pk", + "columns": [ + "id", + "secret" + ] + } + }, + "uniqueConstraints": {} + }, + "user_key": { + "name": "user_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "hashed_password": { + "name": "hashed_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_key_user_id_auth_user_id_fk": { + "name": "user_key_user_id_auth_user_id_fk", + "tableFrom": "user_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_session": { + "name": "user_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "active_expires": { + "name": "active_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idle_expires": { + "name": "idle_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_user_id_auth_user_id_fk": { + "name": "user_session_user_id_auth_user_id_fk", + "tableFrom": "user_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "signup_token": { + "name": "signup_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "used_at": { + "name": "used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "signup_token_user_id_auth_user_id_fk": { + "name": "signup_token_user_id_auth_user_id_fk", + "tableFrom": "signup_token", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "links": { + "name": "links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "links_created_at_index": { + "name": "links_created_at_index", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "links_last_used_index": { + "name": "links_last_used_index", + "columns": [ + "last_used" + ], + "isUnique": false + }, + "links_valid_until_index": { + "name": "links_valid_until_index", + "columns": [ + "valid_until" + ], + "isUnique": false + }, + "links_user_id_index": { + "name": "links_user_id_index", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "links_user_id_auth_user_id_fk": { + "name": "links_user_id_auth_user_id_fk", + "tableFrom": "links", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0014_snapshot.json b/drizzle/meta/0014_snapshot.json new file mode 100644 index 0000000..c3857b3 --- /dev/null +++ b/drizzle/meta/0014_snapshot.json @@ -0,0 +1,446 @@ +{ + "id": "dc4d3e85-880f-4287-a853-0463c7886cb4", + "prevId": "f74c9758-b6ae-4611-8007-a5e449bba755", + "version": "5", + "dialect": "pg", + "tables": { + "link_visit": { + "name": "link_visit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "link_id": { + "name": "link_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "link_visit_link_id_index": { + "name": "link_visit_link_id_index", + "columns": [ + "link_id" + ], + "isUnique": false + }, + "link_visit_timestamp_index": { + "name": "link_visit_timestamp_index", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "link_visit_link_id_links_id_fk": { + "name": "link_visit_link_id_links_id_fk", + "tableFrom": "link_visit", + "tableTo": "links", + "columnsFrom": [ + "link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_key_user_id_idx": { + "name": "api_key_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_key_created_at_idx": { + "name": "api_key_created_at_idx", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_key_user_id_auth_user_id_fk": { + "name": "api_key_user_id_auth_user_id_fk", + "tableFrom": "api_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "api_key_id_secret_pk": { + "name": "api_key_id_secret_pk", + "columns": [ + "id", + "secret" + ] + } + }, + "uniqueConstraints": {} + }, + "user_key": { + "name": "user_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "hashed_password": { + "name": "hashed_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_key_user_id_auth_user_id_fk": { + "name": "user_key_user_id_auth_user_id_fk", + "tableFrom": "user_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_session": { + "name": "user_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "active_expires": { + "name": "active_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idle_expires": { + "name": "idle_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_user_id_auth_user_id_fk": { + "name": "user_session_user_id_auth_user_id_fk", + "tableFrom": "user_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "signup_token": { + "name": "signup_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "signup_token_user_id_auth_user_id_fk": { + "name": "signup_token_user_id_auth_user_id_fk", + "tableFrom": "signup_token", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "links": { + "name": "links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used": { + "name": "last_used", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "links_created_at_index": { + "name": "links_created_at_index", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "links_last_used_index": { + "name": "links_last_used_index", + "columns": [ + "last_used" + ], + "isUnique": false + }, + "links_valid_until_index": { + "name": "links_valid_until_index", + "columns": [ + "valid_until" + ], + "isUnique": false + }, + "links_user_id_index": { + "name": "links_user_id_index", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "links_user_id_auth_user_id_fk": { + "name": "links_user_id_auth_user_id_fk", + "tableFrom": "links", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 3b71517..599a145 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -92,6 +92,20 @@ "when": 1706727055992, "tag": "0012_plain_blonde_phantom", "breakpoints": true + }, + { + "idx": 13, + "version": "5", + "when": 1713389088480, + "tag": "0013_charming_madame_hydra", + "breakpoints": true + }, + { + "idx": 14, + "version": "5", + "when": 1713389414892, + "tag": "0014_simple_psylocke", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/server/database/schema/analytics.ts b/src/lib/server/database/schema/analytics.ts index a26f3bc..0af027f 100644 --- a/src/lib/server/database/schema/analytics.ts +++ b/src/lib/server/database/schema/analytics.ts @@ -11,7 +11,7 @@ export const linkVisit = pgTable( referrer: text('referrer').notNull().default(''), userAgent: text('user_agent').notNull().default(''), - timestamp: timestamp('timestamp').notNull().defaultNow() + timestamp: timestamp('timestamp', { withTimezone: true }).notNull().defaultNow() }, (self) => ({ linkIdIdx: index('link_visit_link_id_index').on(self.linkId), diff --git a/src/lib/server/database/schema/api.ts b/src/lib/server/database/schema/api.ts new file mode 100644 index 0000000..c59eee8 --- /dev/null +++ b/src/lib/server/database/schema/api.ts @@ -0,0 +1,28 @@ +import { pgTable, varchar, text, primaryKey, timestamp, index } from 'drizzle-orm/pg-core'; +import { user } from './auth'; +import { createInsertSchema } from 'drizzle-zod'; +import { z } from 'zod'; + +export const apiKey = pgTable( + 'api_key', + { + id: varchar('id', { length: 15 }), + secret: text('secret'), + userId: varchar('user_id', { length: 15 }) + .notNull() + .references(() => user.id, { onUpdate: 'cascade', onDelete: 'cascade' }), + created_at: timestamp('timestamp', { withTimezone: true }).notNull().defaultNow() + }, + (self) => ({ + pk: primaryKey({ columns: [self.id, self.secret] }), + userIdIdx: index('api_key_user_id_idx').on(self.userId), + createdAtIdx: index('api_key_created_at_idx').on(self.created_at).desc() + }) +); + +export type ApiKey = typeof apiKey.$inferSelect; + +export const apiKeyInsertSchema = createInsertSchema(apiKey, { + id: z.string().length(15), + secret: z.string().min(1) +}); diff --git a/src/lib/server/database/schema/auth.ts b/src/lib/server/database/schema/auth.ts index c5d5e5c..098d042 100644 --- a/src/lib/server/database/schema/auth.ts +++ b/src/lib/server/database/schema/auth.ts @@ -40,8 +40,8 @@ export const signupToken = pgTable('signup_token', { role: text('role', { enum: ['admin', 'member'] }) .notNull() .default('member'), - createdAt: timestamp('created_at').notNull().defaultNow(), - usedAt: timestamp('used_at'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + usedAt: timestamp('used_at', { withTimezone: true }), userId: varchar('user_id', { length: 15 }).references(() => user.id, { onUpdate: 'cascade', onDelete: 'cascade' diff --git a/src/lib/server/database/schema/link.ts b/src/lib/server/database/schema/link.ts index 0198712..4b5dbbd 100644 --- a/src/lib/server/database/schema/link.ts +++ b/src/lib/server/database/schema/link.ts @@ -9,10 +9,10 @@ export const links = pgTable( { id: text('id').primaryKey(), url: text('url').notNull(), - createdAt: timestamp('created_at').notNull().defaultNow(), - lastUsed: timestamp('last_used'), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + lastUsed: timestamp('last_used', { withTimezone: true }), - validUntil: timestamp('valid_until'), + validUntil: timestamp('valid_until', { withTimezone: true }), userId: varchar('user_id', { length: 15 }) .notNull() From fb324161fe3193b27d3f9ea2b7490585032ff7f7 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 00:33:21 +0300 Subject: [PATCH 2/7] make api key id and secret non-null --- drizzle/0015_sloppy_hardball.sql | 2 + drizzle/meta/0015_snapshot.json | 446 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/lib/server/database/schema/api.ts | 4 +- 4 files changed, 457 insertions(+), 2 deletions(-) create mode 100644 drizzle/0015_sloppy_hardball.sql create mode 100644 drizzle/meta/0015_snapshot.json diff --git a/drizzle/0015_sloppy_hardball.sql b/drizzle/0015_sloppy_hardball.sql new file mode 100644 index 0000000..24758ed --- /dev/null +++ b/drizzle/0015_sloppy_hardball.sql @@ -0,0 +1,2 @@ +ALTER TABLE "api_key" ALTER COLUMN "id" SET NOT NULL;--> statement-breakpoint +ALTER TABLE "api_key" ALTER COLUMN "secret" SET NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0015_snapshot.json b/drizzle/meta/0015_snapshot.json new file mode 100644 index 0000000..e338fd7 --- /dev/null +++ b/drizzle/meta/0015_snapshot.json @@ -0,0 +1,446 @@ +{ + "id": "fa9f93f4-7480-41d6-ac8a-6021a3f6c150", + "prevId": "dc4d3e85-880f-4287-a853-0463c7886cb4", + "version": "5", + "dialect": "pg", + "tables": { + "link_visit": { + "name": "link_visit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "link_id": { + "name": "link_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "link_visit_link_id_index": { + "name": "link_visit_link_id_index", + "columns": [ + "link_id" + ], + "isUnique": false + }, + "link_visit_timestamp_index": { + "name": "link_visit_timestamp_index", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "link_visit_link_id_links_id_fk": { + "name": "link_visit_link_id_links_id_fk", + "tableFrom": "link_visit", + "tableTo": "links", + "columnsFrom": [ + "link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_key_user_id_idx": { + "name": "api_key_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_key_created_at_idx": { + "name": "api_key_created_at_idx", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_key_user_id_auth_user_id_fk": { + "name": "api_key_user_id_auth_user_id_fk", + "tableFrom": "api_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "api_key_id_secret_pk": { + "name": "api_key_id_secret_pk", + "columns": [ + "id", + "secret" + ] + } + }, + "uniqueConstraints": {} + }, + "user_key": { + "name": "user_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "hashed_password": { + "name": "hashed_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_key_user_id_auth_user_id_fk": { + "name": "user_key_user_id_auth_user_id_fk", + "tableFrom": "user_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_session": { + "name": "user_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "active_expires": { + "name": "active_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idle_expires": { + "name": "idle_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_user_id_auth_user_id_fk": { + "name": "user_session_user_id_auth_user_id_fk", + "tableFrom": "user_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "signup_token": { + "name": "signup_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "signup_token_user_id_auth_user_id_fk": { + "name": "signup_token_user_id_auth_user_id_fk", + "tableFrom": "signup_token", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "links": { + "name": "links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used": { + "name": "last_used", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "links_created_at_index": { + "name": "links_created_at_index", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "links_last_used_index": { + "name": "links_last_used_index", + "columns": [ + "last_used" + ], + "isUnique": false + }, + "links_valid_until_index": { + "name": "links_valid_until_index", + "columns": [ + "valid_until" + ], + "isUnique": false + }, + "links_user_id_index": { + "name": "links_user_id_index", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "links_user_id_auth_user_id_fk": { + "name": "links_user_id_auth_user_id_fk", + "tableFrom": "links", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 599a145..44a25d8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1713389414892, "tag": "0014_simple_psylocke", "breakpoints": true + }, + { + "idx": 15, + "version": "5", + "when": 1713389570647, + "tag": "0015_sloppy_hardball", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/server/database/schema/api.ts b/src/lib/server/database/schema/api.ts index c59eee8..1b2e16e 100644 --- a/src/lib/server/database/schema/api.ts +++ b/src/lib/server/database/schema/api.ts @@ -6,8 +6,8 @@ import { z } from 'zod'; export const apiKey = pgTable( 'api_key', { - id: varchar('id', { length: 15 }), - secret: text('secret'), + id: varchar('id', { length: 15 }).notNull(), + secret: text('secret').notNull(), userId: varchar('user_id', { length: 15 }) .notNull() .references(() => user.id, { onUpdate: 'cascade', onDelete: 'cascade' }), From f3b5ea2a0c15a9cd88234f75b4e05d5f96a74bf8 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 01:03:40 +0300 Subject: [PATCH 3/7] WIP api keys --- drizzle/0016_many_doomsday.sql | 3 + drizzle/meta/0016_snapshot.json | 446 ++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/app.d.ts | 4 + src/hooks.server.ts | 19 +- src/lib/server/database/handlers/api.ts | 12 + src/lib/server/database/schema/api.ts | 4 +- src/routes/(app)/me/+page.server.ts | 4 +- src/routes/(app)/me/+page.svelte | 29 +- src/routes/(app)/me/ApiKeysTable.svelte | 27 ++ src/routes/(app)/me/LinksTable.svelte | 194 +++++------ src/routes/api/links/+server.ts | 24 ++ src/routes/api/links/[id]/+server.ts | 24 ++ 13 files changed, 687 insertions(+), 110 deletions(-) create mode 100644 drizzle/0016_many_doomsday.sql create mode 100644 drizzle/meta/0016_snapshot.json create mode 100644 src/lib/server/database/handlers/api.ts create mode 100644 src/routes/(app)/me/ApiKeysTable.svelte create mode 100644 src/routes/api/links/+server.ts create mode 100644 src/routes/api/links/[id]/+server.ts diff --git a/drizzle/0016_many_doomsday.sql b/drizzle/0016_many_doomsday.sql new file mode 100644 index 0000000..8a3864c --- /dev/null +++ b/drizzle/0016_many_doomsday.sql @@ -0,0 +1,3 @@ +ALTER TABLE "api_key" RENAME COLUMN "timestamp" TO "created_at";--> statement-breakpoint +DROP INDEX IF EXISTS "api_key_created_at_idx";--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "api_key_created_at_idx" ON "api_key" ("created_at"); \ No newline at end of file diff --git a/drizzle/meta/0016_snapshot.json b/drizzle/meta/0016_snapshot.json new file mode 100644 index 0000000..38fc058 --- /dev/null +++ b/drizzle/meta/0016_snapshot.json @@ -0,0 +1,446 @@ +{ + "id": "c20aa609-249d-41ba-a163-93c0a90b1997", + "prevId": "fa9f93f4-7480-41d6-ac8a-6021a3f6c150", + "version": "5", + "dialect": "pg", + "tables": { + "link_visit": { + "name": "link_visit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "link_id": { + "name": "link_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "timestamp": { + "name": "timestamp", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "link_visit_link_id_index": { + "name": "link_visit_link_id_index", + "columns": [ + "link_id" + ], + "isUnique": false + }, + "link_visit_timestamp_index": { + "name": "link_visit_timestamp_index", + "columns": [ + "timestamp" + ], + "isUnique": false + } + }, + "foreignKeys": { + "link_visit_link_id_links_id_fk": { + "name": "link_visit_link_id_links_id_fk", + "tableFrom": "link_visit", + "tableTo": "links", + "columnsFrom": [ + "link_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "api_key_user_id_idx": { + "name": "api_key_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "api_key_created_at_idx": { + "name": "api_key_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "api_key_user_id_auth_user_id_fk": { + "name": "api_key_user_id_auth_user_id_fk", + "tableFrom": "api_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": { + "api_key_id_secret_pk": { + "name": "api_key_id_secret_pk", + "columns": [ + "id", + "secret" + ] + } + }, + "uniqueConstraints": {} + }, + "user_key": { + "name": "user_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(255)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "hashed_password": { + "name": "hashed_password", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_key_user_id_auth_user_id_fk": { + "name": "user_key_user_id_auth_user_id_fk", + "tableFrom": "user_key", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_session": { + "name": "user_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(128)", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + }, + "active_expires": { + "name": "active_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idle_expires": { + "name": "idle_expires", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_session_user_id_auth_user_id_fk": { + "name": "user_session_user_id_auth_user_id_fk", + "tableFrom": "user_session", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "signup_token": { + "name": "signup_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "signup_token_user_id_auth_user_id_fk": { + "name": "signup_token_user_id_auth_user_id_fk", + "tableFrom": "signup_token", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "auth_user": { + "name": "auth_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "varchar(15)", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "auth_user_email_unique": { + "name": "auth_user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "links": { + "name": "links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used": { + "name": "last_used", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(15)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "links_created_at_index": { + "name": "links_created_at_index", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "links_last_used_index": { + "name": "links_last_used_index", + "columns": [ + "last_used" + ], + "isUnique": false + }, + "links_valid_until_index": { + "name": "links_valid_until_index", + "columns": [ + "valid_until" + ], + "isUnique": false + }, + "links_user_id_index": { + "name": "links_user_id_index", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "links_user_id_auth_user_id_fk": { + "name": "links_user_id_auth_user_id_fk", + "tableFrom": "links", + "tableTo": "auth_user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 44a25d8..28ba732 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1713389570647, "tag": "0015_sloppy_hardball", "breakpoints": true + }, + { + "idx": 16, + "version": "5", + "when": 1713390975363, + "tag": "0016_many_doomsday", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index d844779..cebb903 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,10 +1,14 @@ // See https://kit.svelte.dev/docs/types#app + +import type { ApiKey } from '$lib/server/database/schema/api'; + // for information about these interfaces declare global { namespace App { // interface Error {} interface Locals { auth: import('lucia').AuthRequest; + apiKey: ApiKey; } // interface PageData {} // interface PageState {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index bd164dd..71e158b 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,7 +1,24 @@ import { auth } from '$lib/server/auth/lucia'; -import { type Handle } from '@sveltejs/kit'; +import { getApiKeyBySecret } from '$lib/server/database/handlers/api'; +import { error, type Handle } from '@sveltejs/kit'; export const handle: Handle = async ({ event, resolve }) => { + if (event.url.pathname.startsWith('/api')) { + const token = event.request.headers.get('authorization'); + if (!token) { + error(401, 'unauthorized'); + } + + const apiKey = await getApiKeyBySecret(token); + if (!apiKey) { + error(401, 'unauthorized'); + } + + event.locals.apiKey = apiKey; + + return await resolve(event); + } + // we can pass `event` because we used the SvelteKit middleware event.locals.auth = auth.handleRequest(event); diff --git a/src/lib/server/database/handlers/api.ts b/src/lib/server/database/handlers/api.ts new file mode 100644 index 0000000..619e5d1 --- /dev/null +++ b/src/lib/server/database/handlers/api.ts @@ -0,0 +1,12 @@ +import { eq } from 'drizzle-orm'; +import { db } from '..'; +import { apiKey, type ApiKey } from '../schema/api'; + +export const getApiKeyBySecret = async (secret: string): Promise => { + const rows = await db.select().from(apiKey).where(eq(apiKey.secret, secret)).limit(1); + return rows?.[0] ?? null; +}; + +export const getUserApiKeys = async (userId: string): Promise => { + return db.select().from(apiKey).where(eq(apiKey.userId, userId)); +}; diff --git a/src/lib/server/database/schema/api.ts b/src/lib/server/database/schema/api.ts index 1b2e16e..615a069 100644 --- a/src/lib/server/database/schema/api.ts +++ b/src/lib/server/database/schema/api.ts @@ -11,12 +11,12 @@ export const apiKey = pgTable( userId: varchar('user_id', { length: 15 }) .notNull() .references(() => user.id, { onUpdate: 'cascade', onDelete: 'cascade' }), - created_at: timestamp('timestamp', { withTimezone: true }).notNull().defaultNow() + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow() }, (self) => ({ pk: primaryKey({ columns: [self.id, self.secret] }), userIdIdx: index('api_key_user_id_idx').on(self.userId), - createdAtIdx: index('api_key_created_at_idx').on(self.created_at).desc() + createdAtIdx: index('api_key_created_at_idx').on(self.createdAt).desc() }) ); diff --git a/src/routes/(app)/me/+page.server.ts b/src/routes/(app)/me/+page.server.ts index 0708f65..8a8f942 100644 --- a/src/routes/(app)/me/+page.server.ts +++ b/src/routes/(app)/me/+page.server.ts @@ -3,6 +3,7 @@ import { error, redirect } from '@sveltejs/kit'; import { deleteLink, getAllUserLinks } from '$lib/server/database/handlers/links'; import { getOverallLinkStatistics } from '$lib/server/database/handlers/analytics'; import { deleteUser } from '$lib/server/database/handlers/user'; +import { getUserApiKeys } from '$lib/server/database/handlers/api'; export const load = (async ({ parent }) => { const { session } = await parent(); @@ -13,7 +14,8 @@ export const load = (async ({ parent }) => { return { links: await getAllUserLinks(session.user.id), - stats: getOverallLinkStatistics(session.user.id) + stats: getOverallLinkStatistics(session.user.id), + apiKeys: await getUserApiKeys(session.user.id) }; }) satisfies PageServerLoad; diff --git a/src/routes/(app)/me/+page.svelte b/src/routes/(app)/me/+page.svelte index 0f9663a..6aa8625 100644 --- a/src/routes/(app)/me/+page.svelte +++ b/src/routes/(app)/me/+page.svelte @@ -1,10 +1,11 @@ + + + + + ID + Secret + Created + + + + + + {#each keys as key (key.id)} + + {key.id} + {key.secret} + {key.createdAt.toLocaleString()} + + {/each} + + diff --git a/src/routes/(app)/me/LinksTable.svelte b/src/routes/(app)/me/LinksTable.svelte index a636478..36bcb02 100644 --- a/src/routes/(app)/me/LinksTable.svelte +++ b/src/routes/(app)/me/LinksTable.svelte @@ -6,7 +6,6 @@ IconExternalLink, IconTrash } from '@tabler/icons-svelte'; - import * as Card from '$lib/components/ui/card'; import * as Table from '$lib/components/ui/table'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import * as AlertDialog from '$lib/components/ui/alert-dialog'; @@ -22,114 +21,101 @@ export let links: Link[]; - - - My Links ({links.length}) - +{#if links.length > 0} + + + + Shortened link + Original link + Last visited + Created + + + - - {#if links.length > 0} - - - - Shortened link - Original link - Last visited - Created - - - + + {#each links as link (link.id)} + {@const url = new URL(`${PUBLIC_BASEURL}/${link.id}`)} + + +
+ + {link.id} + - - {#each links as link (link.id)} - {@const url = new URL(`${PUBLIC_BASEURL}/${link.id}`)} - - - + + + + {link.url} + + + + {link.lastUsed ? dayjs().to(link.lastUsed) : '-'} + + + {link.createdAt.toLocaleString()} + + + + + + + + + + + Edit + - - - -
-
- - - {link.url} - - - - {link.lastUsed ? dayjs().to(link.lastUsed) : '-'} - - - {link.createdAt.toLocaleString()} - - - - - - - - - - - Edit - + + + Analytics + - - - Analytics - + + + + Delete + + + + - - - - Delete - - - - + + + Are you sure? + + You are about to permanently delete {link.id}. This cannot be undone. + + - - - Are you sure? - - You are about to permanently delete {link.id}. This cannot be undone. - - + + Cancel - - Cancel +
+ - - - - - Delete - -
-
-
-
-
-
- {/each} -
-
- {:else} -

- You haven't shortened any links yet. - Shorten link -

- {/if} -
-
+ + Delete + + + + + + + + {/each} + + +{:else} +

+ You haven't shortened any links yet. + Shorten link +

+{/if} diff --git a/src/routes/api/links/+server.ts b/src/routes/api/links/+server.ts new file mode 100644 index 0000000..00e7b75 --- /dev/null +++ b/src/routes/api/links/+server.ts @@ -0,0 +1,24 @@ +import { getAllUserLinks, insertLink } from '$lib/server/database/handlers/links.js'; +import { linkInsertSchema } from '$lib/server/database/schema/link.js'; +import { error, json } from '@sveltejs/kit'; + +/** + * GET /api/links + */ +export const GET = async ({ locals }) => { + return json(await getAllUserLinks(locals.apiKey.userId)); +}; + +/** + * POST /api/links + */ +export const POST = async ({ locals, request }) => { + const body = linkInsertSchema.omit({ userId: true }).safeParse(await request.json()); + if (!body.success) { + error(400, body.error.message); + } + + const link = await insertLink({ ...body.data, userId: locals.apiKey.userId }); + + return json(link, { status: 201 }); +}; diff --git a/src/routes/api/links/[id]/+server.ts b/src/routes/api/links/[id]/+server.ts new file mode 100644 index 0000000..7e513df --- /dev/null +++ b/src/routes/api/links/[id]/+server.ts @@ -0,0 +1,24 @@ +import { deleteLink, getUserLink } from '$lib/server/database/handlers/links.js'; +import { error, json } from '@sveltejs/kit'; + +/** + * GET /api/links/[id] + */ +export const GET = async ({ locals, params }) => { + return json(await getUserLink(params.id, locals.apiKey.userId)); +}; + +/** + * PATCH /api/links/[id] + */ +export const PATCH = async () => { + return error(501, 'not implemented'); +}; + +/** + * DELETE /api/links/[id] + */ +export const DELETE = async ({ params }) => { + await deleteLink(params.id); // TODO: make sure user owns the link + return new Response(null, { status: 200 }); +}; From 02eb4666e027fc27d83217216293f7f4241d1ba5 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 23:06:22 +0300 Subject: [PATCH 4/7] add basic link API and API keys management --- package-lock.json | 24 ++- package.json | 4 +- src/app.d.ts | 4 + src/lib/components/Crumbs.svelte | 29 +++ .../ui/breadcrumb/breadcrumb-ellipsis.svelte | 24 +++ .../ui/breadcrumb/breadcrumb-item.svelte | 16 ++ .../ui/breadcrumb/breadcrumb-link.svelte | 31 +++ .../ui/breadcrumb/breadcrumb-list.svelte | 23 ++ .../ui/breadcrumb/breadcrumb-page.svelte | 23 ++ .../ui/breadcrumb/breadcrumb-separator.svelte | 25 +++ .../ui/breadcrumb/breadcrumb.svelte | 15 ++ src/lib/components/ui/breadcrumb/index.ts | 25 +++ src/lib/server/constants.ts | 5 +- src/lib/server/database/handlers/api.ts | 25 ++- src/lib/server/database/handlers/links.ts | 12 +- src/lib/server/database/schema/api.ts | 4 +- src/routes/(app)/+page.svelte | 19 +- src/routes/(app)/me/+layout.svelte | 11 + src/routes/(app)/me/+page.svelte | 196 +++++++++--------- src/routes/(app)/me/ApiKeysTable.svelte | 27 --- src/routes/(app)/me/admin/+page.svelte | 22 +- src/routes/(app)/me/api/+page.server.ts | 43 ++++ src/routes/(app)/me/api/+page.svelte | 118 +++++++++++ src/routes/(app)/me/api/ApiKeysTable.svelte | 60 ++++++ .../(app)/me/links/[id]/edit/+page.server.ts | 11 +- src/routes/api/links/+server.ts | 8 +- src/routes/api/links/[id]/+server.ts | 15 +- 27 files changed, 651 insertions(+), 168 deletions(-) create mode 100644 src/lib/components/Crumbs.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb-item.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb-link.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb-list.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb-page.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte create mode 100644 src/lib/components/ui/breadcrumb/breadcrumb.svelte create mode 100644 src/lib/components/ui/breadcrumb/index.ts create mode 100644 src/routes/(app)/me/+layout.svelte delete mode 100644 src/routes/(app)/me/ApiKeysTable.svelte create mode 100644 src/routes/(app)/me/api/+page.server.ts create mode 100644 src/routes/(app)/me/api/+page.svelte create mode 100644 src/routes/(app)/me/api/ApiKeysTable.svelte diff --git a/package-lock.json b/package-lock.json index bd41c92..37eaf60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "nprogress": "^0.2.0", "pg": "^8.11.3", "radix-icons-svelte": "^1.2.1", + "svelte-radix": "^1.1.0", "svelte-sonner": "^0.3.11", "tailwind-merge": "^2.2.0", "tailwind-variants": "^0.1.20", @@ -57,7 +58,8 @@ "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^5.0.3", - "vitest": "^1.2.0" + "vitest": "^1.2.0", + "zod-validation-error": "^3.1.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -5976,6 +5978,14 @@ } } }, + "node_modules/svelte-radix": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/svelte-radix/-/svelte-radix-1.1.0.tgz", + "integrity": "sha512-kyE9wZiJV937INGb+wiBkAjmGtQUUYRPkVL2Q+/gj+9Vog1Ewd2wNvNmpNMUd+c+euxoc5u5YZMuCUgky9EUPw==", + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0" + } + }, "node_modules/svelte-sonner": { "version": "0.3.11", "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.11.tgz", @@ -6748,6 +6758,18 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-validation-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.1.0.tgz", + "integrity": "sha512-zujS6HqJjMZCsvjfbnRs7WI3PXN39ovTcY1n8a+KTm4kOH0ZXYsNiJkH1odZf4xZKMkBDL7M2rmQ913FCS1p9w==", + "dev": true, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } } } } diff --git a/package.json b/package.json index 089f284..7b0c6a9 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "tslib": "^2.4.1", "typescript": "^5.0.0", "vite": "^5.0.3", - "vitest": "^1.2.0" + "vitest": "^1.2.0", + "zod-validation-error": "^3.1.0" }, "type": "module", "dependencies": { @@ -64,6 +65,7 @@ "nprogress": "^0.2.0", "pg": "^8.11.3", "radix-icons-svelte": "^1.2.1", + "svelte-radix": "^1.1.0", "svelte-sonner": "^0.3.11", "tailwind-merge": "^2.2.0", "tailwind-variants": "^0.1.20", diff --git a/src/app.d.ts b/src/app.d.ts index cebb903..2ea2aac 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -8,6 +8,10 @@ declare global { // interface Error {} interface Locals { auth: import('lucia').AuthRequest; + + /** + * Only used inside /api routes!! + */ apiKey: ApiKey; } // interface PageData {} diff --git a/src/lib/components/Crumbs.svelte b/src/lib/components/Crumbs.svelte new file mode 100644 index 0000000..8c4473a --- /dev/null +++ b/src/lib/components/Crumbs.svelte @@ -0,0 +1,29 @@ + + + + + {#each links as link, i (link.label)} + + {#if i === links.length - 1} + + {link.label} + + {:else} + + {link.label} + + {/if} + + + {#if i < links.length - 1} + + {/if} + {/each} + + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte new file mode 100644 index 0000000..d733e9b --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-ellipsis.svelte @@ -0,0 +1,24 @@ + + + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte new file mode 100644 index 0000000..8e8b187 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-item.svelte @@ -0,0 +1,16 @@ + + +
  • + +
  • diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte new file mode 100644 index 0000000..e743768 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-link.svelte @@ -0,0 +1,31 @@ + + +{#if asChild} + +{:else} + + + +{/if} diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte new file mode 100644 index 0000000..7f36141 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-list.svelte @@ -0,0 +1,23 @@ + + +
      + +
    diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte new file mode 100644 index 0000000..757f60e --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-page.svelte @@ -0,0 +1,23 @@ + + + + + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte new file mode 100644 index 0000000..681ef99 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb-separator.svelte @@ -0,0 +1,25 @@ + + + diff --git a/src/lib/components/ui/breadcrumb/breadcrumb.svelte b/src/lib/components/ui/breadcrumb/breadcrumb.svelte new file mode 100644 index 0000000..104f8b0 --- /dev/null +++ b/src/lib/components/ui/breadcrumb/breadcrumb.svelte @@ -0,0 +1,15 @@ + + + diff --git a/src/lib/components/ui/breadcrumb/index.ts b/src/lib/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..dc914ec --- /dev/null +++ b/src/lib/components/ui/breadcrumb/index.ts @@ -0,0 +1,25 @@ +import Root from "./breadcrumb.svelte"; +import Ellipsis from "./breadcrumb-ellipsis.svelte"; +import Item from "./breadcrumb-item.svelte"; +import Separator from "./breadcrumb-separator.svelte"; +import Link from "./breadcrumb-link.svelte"; +import List from "./breadcrumb-list.svelte"; +import Page from "./breadcrumb-page.svelte"; + +export { + Root, + Ellipsis, + Item, + Separator, + Link, + List, + Page, + // + Root as Breadcrumb, + Ellipsis as BreadcrumbEllipsis, + Item as BreadcrumbItem, + Separator as BreadcrumbSeparator, + Link as BreadcrumbLink, + List as BreadcrumbList, + Page as BreadcrumbPage, +}; diff --git a/src/lib/server/constants.ts b/src/lib/server/constants.ts index 0efaefa..95b8b24 100644 --- a/src/lib/server/constants.ts +++ b/src/lib/server/constants.ts @@ -1 +1,4 @@ -export const RESERVED_LINK_IDS = ['auth', 'me']; +/** + * These are all the top-level routes that pieni.link has, and cannot therefore be used as link IDs. + */ +export const RESERVED_LINK_IDS = ['about', 'me', 'api', 'auth']; diff --git a/src/lib/server/database/handlers/api.ts b/src/lib/server/database/handlers/api.ts index 619e5d1..f0081c5 100644 --- a/src/lib/server/database/handlers/api.ts +++ b/src/lib/server/database/handlers/api.ts @@ -1,12 +1,33 @@ -import { eq } from 'drizzle-orm'; +import { and, eq } from 'drizzle-orm'; import { db } from '..'; import { apiKey, type ApiKey } from '../schema/api'; +import { nanoid } from 'nanoid'; export const getApiKeyBySecret = async (secret: string): Promise => { const rows = await db.select().from(apiKey).where(eq(apiKey.secret, secret)).limit(1); return rows?.[0] ?? null; }; -export const getUserApiKeys = async (userId: string): Promise => { +export const getUserApiKeys = async (userId: string): Promise[]> => { + return db + .select({ id: apiKey.id, userId: apiKey.userId, createdAt: apiKey.createdAt }) + .from(apiKey) + .where(eq(apiKey.userId, userId)); +}; + +export const getUserApiKeysUnsafe = async (userId: string): Promise => { return db.select().from(apiKey).where(eq(apiKey.userId, userId)); }; + +export const createUserApiKey = async (userId: string): Promise => { + const rows = await db + .insert(apiKey) + .values({ userId, id: nanoid(15), secret: nanoid(24) }) + .returning(); + + return rows[0]; +}; + +export const deleteUserApiKey = async (keyId: string, userId: string) => { + await db.delete(apiKey).where(and(eq(apiKey.id, keyId), eq(apiKey.userId, userId))); +}; diff --git a/src/lib/server/database/handlers/links.ts b/src/lib/server/database/handlers/links.ts index f5edc85..3375516 100644 --- a/src/lib/server/database/handlers/links.ts +++ b/src/lib/server/database/handlers/links.ts @@ -50,8 +50,16 @@ export const getUserLink = async (linkId: Link['id'], userId: AuthUser['id']): P return rows[0]; }; -export const updateLink = async (linkId: Link['id'], data: LinkUpdate): Promise => { - const rows = await db.update(links).set(data).where(eq(links.id, linkId)).returning(); +export const updateUserLink = async ( + linkId: Link['id'], + userId: string, + data: LinkUpdate +): Promise => { + const rows = await db + .update(links) + .set(data) + .where(and(eq(links.id, linkId), eq(links.userId, userId))) + .returning(); if (rows.length === 0) { error(500, 'failed to update'); diff --git a/src/lib/server/database/schema/api.ts b/src/lib/server/database/schema/api.ts index 615a069..ddba3c2 100644 --- a/src/lib/server/database/schema/api.ts +++ b/src/lib/server/database/schema/api.ts @@ -1,6 +1,6 @@ import { pgTable, varchar, text, primaryKey, timestamp, index } from 'drizzle-orm/pg-core'; import { user } from './auth'; -import { createInsertSchema } from 'drizzle-zod'; +import { createInsertSchema, createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; export const apiKey = pgTable( @@ -21,7 +21,9 @@ export const apiKey = pgTable( ); export type ApiKey = typeof apiKey.$inferSelect; +export const apiKeySchema = createSelectSchema(apiKey); +export type ApiKeyInsert = typeof apiKey.$inferInsert; export const apiKeyInsertSchema = createInsertSchema(apiKey, { id: z.string().length(15), secret: z.string().min(1) diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 5d46c80..887cd95 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -85,23 +85,8 @@ placeholder="Paste a long link here to shorten..." on:paste={handleOnPaste} disabled={loading} - class="text-md rounded-r-none p-5" + class="text-md p-5" /> - - {:else} - + {/if} diff --git a/src/routes/(app)/me/+layout.svelte b/src/routes/(app)/me/+layout.svelte new file mode 100644 index 0000000..a93ee02 --- /dev/null +++ b/src/routes/(app)/me/+layout.svelte @@ -0,0 +1,11 @@ + + +
    + +
    diff --git a/src/routes/(app)/me/+page.svelte b/src/routes/(app)/me/+page.svelte index 6aa8625..08eecc6 100644 --- a/src/routes/(app)/me/+page.svelte +++ b/src/routes/(app)/me/+page.svelte @@ -5,8 +5,8 @@ import type { PageData } from './$types'; import LinksTable from './LinksTable.svelte'; import * as Card from '$lib/components/ui/card'; - import ApiKeysTable from './ApiKeysTable.svelte'; import OverallStatisticsOverview from './OverallStatisticsOverview.svelte'; + import Crumbs from '$lib/components/Crumbs.svelte'; export let data: PageData; @@ -15,105 +15,103 @@ Me -
    -
    -
    - {data.session?.user.name} - - -

    {data.session?.user.name}

    - {data.session?.user.role} -
    + + + + +
    +
    + {data.session?.user.name} + + +

    {data.session?.user.name}

    + {data.session?.user.role} +
    +
    + +
    - -
    - -
    - {#if data.session?.user.role === 'admin'} - - {/if} - - -
    - - {#await data.stats} - - {:then stats} - - {/await} - - - - My Links ({data.links.length}) - - - - - - - - - API Keys - Programmatic access to your links - - - - - - - - - - Danger zone - Be careful now! - - - - - - - - - - - Are you sure? - - If you delete your account, all of the links associated with it will also be deleted - permanently. - - - - -
    - Keep account - -
    -
    -
    -
    -
    -
    -
    + {#if data.session?.user.role === 'admin'} + + {/if} +
    + + + +{#await data.stats} + +{:then stats} + +{/await} + + + + My Links + + + + + + + + + + Danger zone + Be careful now! + + + + + + + + + + + Are you sure? + + If you delete your account, all of the links associated with it will also be deleted + permanently. + + + + +
    + Keep account + +
    +
    +
    +
    +
    +
    diff --git a/src/routes/(app)/me/ApiKeysTable.svelte b/src/routes/(app)/me/ApiKeysTable.svelte deleted file mode 100644 index b647e3b..0000000 --- a/src/routes/(app)/me/ApiKeysTable.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - ID - Secret - Created - - - - - - {#each keys as key (key.id)} - - {key.id} - {key.secret} - {key.createdAt.toLocaleString()} - - {/each} - - diff --git a/src/routes/(app)/me/admin/+page.svelte b/src/routes/(app)/me/admin/+page.svelte index 7180985..df352a3 100644 --- a/src/routes/(app)/me/admin/+page.svelte +++ b/src/routes/(app)/me/admin/+page.svelte @@ -1,7 +1,6 @@ + + + + + + + API Keys + Give other applications access to Pieni.link's data + + +
    + +
    +
    + + { + if (!isOpen) dialogData = null; + }} + > + + + API key created + + Make sure to save the below secret somewhere, as you won't be able to see it after closing + this dialog. + + + +
    + + + + + + + + +
    + + + + +
    +
    + + + {#if data.apiKeys.length > 0} + + {/if} + +
    diff --git a/src/routes/(app)/me/api/ApiKeysTable.svelte b/src/routes/(app)/me/api/ApiKeysTable.svelte new file mode 100644 index 0000000..23365ae --- /dev/null +++ b/src/routes/(app)/me/api/ApiKeysTable.svelte @@ -0,0 +1,60 @@ + + + + + + ID + Created + + + + + + {#each keys as key (key.id)} + + {key.id} + {key.createdAt.toLocaleString()} + + + + + + + + + Are you sure? + + Any application using this API key will not work anymore. + + + + +
    + + + Cancel + + +
    +
    +
    +
    +
    +
    + {/each} +
    +
    diff --git a/src/routes/(app)/me/links/[id]/edit/+page.server.ts b/src/routes/(app)/me/links/[id]/edit/+page.server.ts index ec6378f..be7b360 100644 --- a/src/routes/(app)/me/links/[id]/edit/+page.server.ts +++ b/src/routes/(app)/me/links/[id]/edit/+page.server.ts @@ -1,12 +1,17 @@ import { RESERVED_LINK_IDS } from '$lib/server/constants'; -import { updateLink } from '$lib/server/database/handlers/links.js'; +import { updateUserLink } from '$lib/server/database/handlers/links.js'; import { db } from '$lib/server/database/index.js'; import { linkUpdateSchema, links, type Link } from '$lib/server/database/schema/link.js'; import { error, redirect } from '@sveltejs/kit'; import { and, eq } from 'drizzle-orm'; export const actions = { - update: async ({ request, params }) => { + update: async ({ request, params, locals }) => { + const session = await locals.auth.validate(); + if (!session) { + error(401, 'unauthorized'); + } + const raw = Object.fromEntries(await request.formData()); const body = linkUpdateSchema.safeParse(raw); @@ -21,7 +26,7 @@ export const actions = { let updated: Link; try { - updated = await updateLink(params.id, body.data); + updated = await updateUserLink(params.id, session.user.id, body.data); } catch (err) { error(500, err as Error); } diff --git a/src/routes/api/links/+server.ts b/src/routes/api/links/+server.ts index 00e7b75..f1c865c 100644 --- a/src/routes/api/links/+server.ts +++ b/src/routes/api/links/+server.ts @@ -1,6 +1,7 @@ import { getAllUserLinks, insertLink } from '$lib/server/database/handlers/links.js'; import { linkInsertSchema } from '$lib/server/database/schema/link.js'; import { error, json } from '@sveltejs/kit'; +import { fromZodError } from 'zod-validation-error'; /** * GET /api/links @@ -13,9 +14,12 @@ export const GET = async ({ locals }) => { * POST /api/links */ export const POST = async ({ locals, request }) => { - const body = linkInsertSchema.omit({ userId: true }).safeParse(await request.json()); + const body = linkInsertSchema + .omit({ userId: true }) + .safeParse(await request.json().catch(() => null)); + if (!body.success) { - error(400, body.error.message); + error(400, fromZodError(body.error).message); } const link = await insertLink({ ...body.data, userId: locals.apiKey.userId }); diff --git a/src/routes/api/links/[id]/+server.ts b/src/routes/api/links/[id]/+server.ts index 7e513df..1e3599e 100644 --- a/src/routes/api/links/[id]/+server.ts +++ b/src/routes/api/links/[id]/+server.ts @@ -1,5 +1,7 @@ -import { deleteLink, getUserLink } from '$lib/server/database/handlers/links.js'; +import { deleteLink, getUserLink, updateUserLink } from '$lib/server/database/handlers/links.js'; +import { linkUpdateSchema } from '$lib/server/database/schema/link.js'; import { error, json } from '@sveltejs/kit'; +import { fromZodError } from 'zod-validation-error'; /** * GET /api/links/[id] @@ -11,8 +13,15 @@ export const GET = async ({ locals, params }) => { /** * PATCH /api/links/[id] */ -export const PATCH = async () => { - return error(501, 'not implemented'); +export const PATCH = async ({ request, params, locals }) => { + const parsed = linkUpdateSchema.safeParse(await request.json().catch(() => null)); + if (!parsed.success) { + error(400, fromZodError(parsed.error)); + } + + const updated = await updateUserLink(params.id, locals.apiKey.userId, parsed.data); + + return json(updated); }; /** From 366067342497115e516289c58694de8be09a1fc8 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 23:14:08 +0300 Subject: [PATCH 5/7] order api keys by date, adjust api key createdAt formatting --- src/lib/server/database/handlers/api.ts | 5 +++-- src/routes/(app)/me/api/ApiKeysTable.svelte | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/lib/server/database/handlers/api.ts b/src/lib/server/database/handlers/api.ts index f0081c5..faa30b4 100644 --- a/src/lib/server/database/handlers/api.ts +++ b/src/lib/server/database/handlers/api.ts @@ -1,4 +1,4 @@ -import { and, eq } from 'drizzle-orm'; +import { and, desc, eq } from 'drizzle-orm'; import { db } from '..'; import { apiKey, type ApiKey } from '../schema/api'; import { nanoid } from 'nanoid'; @@ -12,7 +12,8 @@ export const getUserApiKeys = async (userId: string): Promise => { diff --git a/src/routes/(app)/me/api/ApiKeysTable.svelte b/src/routes/(app)/me/api/ApiKeysTable.svelte index 23365ae..2ed8c8f 100644 --- a/src/routes/(app)/me/api/ApiKeysTable.svelte +++ b/src/routes/(app)/me/api/ApiKeysTable.svelte @@ -6,6 +6,7 @@ import * as AlertDialog from '$lib/components/ui/alert-dialog'; import { enhance } from '$app/forms'; import { page } from '$app/stores'; + import dayjs from 'dayjs'; export let keys: Omit[]; @@ -22,8 +23,10 @@ {#each keys as key (key.id)} - {key.id} - {key.createdAt.toLocaleString()} + +
    {key.id}
    +
    + {dayjs(key.createdAt).format('MMM DD YYYY HH:mm:ss')} From 111d0474a4b0e9182af311d6b9110dac88f6bd24 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 23:14:50 +0300 Subject: [PATCH 6/7] remove unused code from layout --- src/routes/(app)/me/+layout.svelte | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/routes/(app)/me/+layout.svelte b/src/routes/(app)/me/+layout.svelte index a93ee02..e2dc0e2 100644 --- a/src/routes/(app)/me/+layout.svelte +++ b/src/routes/(app)/me/+layout.svelte @@ -1,11 +1,3 @@ - -
    From 824ba72ecb21a8048f0256413ad15abb132a5b80 Mon Sep 17 00:00:00 2001 From: Matias Kumpulainen Date: Thu, 18 Apr 2024 23:16:08 +0300 Subject: [PATCH 7/7] fix session null checks --- src/routes/(app)/me/api/+page.server.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/routes/(app)/me/api/+page.server.ts b/src/routes/(app)/me/api/+page.server.ts index 43c3c6c..3a68712 100644 --- a/src/routes/(app)/me/api/+page.server.ts +++ b/src/routes/(app)/me/api/+page.server.ts @@ -7,8 +7,11 @@ import { error } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; import { apiKeySchema } from '$lib/server/database/schema/api'; -export const load = (async ({ parent }) => { - const { session } = await parent(); +export const load = (async ({ locals }) => { + const session = await locals.auth.validate(); + if (!session) { + error(401, 'unauthorized'); + } return { apiKeys: await getUserApiKeys(session.user.id)