feat: add share schema and Discord account reconciliation
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from "./schema/account";
|
||||
export * from "./schema/form";
|
||||
export * from "./schema/video";
|
||||
export * from "./schema/share";
|
||||
|
||||
@@ -4,12 +4,15 @@ import {
|
||||
timestamp,
|
||||
integer,
|
||||
primaryKey,
|
||||
uniqueIndex,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import type { AdapterAccount } from "next-auth/adapters";
|
||||
|
||||
export const accountSchema = pgSchema("account");
|
||||
|
||||
export const users = accountSchema.table("user", {
|
||||
export const users = accountSchema.table(
|
||||
"user",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
@@ -18,7 +21,9 @@ export const users = accountSchema.table("user", {
|
||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
||||
image: text("image"),
|
||||
discordId: text("discord_id"),
|
||||
});
|
||||
},
|
||||
(user) => [uniqueIndex("user_discord_id_unique").on(user.discordId)]
|
||||
);
|
||||
|
||||
export const accounts = accountSchema.table(
|
||||
"account",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import {
|
||||
index,
|
||||
pgSchema,
|
||||
text,
|
||||
timestamp,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { uuidv7 } from "uuidv7";
|
||||
import { users } from "./account";
|
||||
import { cdn } from "./form";
|
||||
|
||||
export const shareSchema = pgSchema("share");
|
||||
|
||||
export type ShareSource = "web" | "bot";
|
||||
|
||||
const SHARE_ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
export function createShareId() {
|
||||
let id = "";
|
||||
while (id.length < 8) {
|
||||
for (const value of randomBytes(8)) {
|
||||
if (value >= 252) continue;
|
||||
id += SHARE_ID_ALPHABET[value % SHARE_ID_ALPHABET.length];
|
||||
if (id.length === 8) break;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
export const shareTexts = shareSchema.table(
|
||||
"text",
|
||||
{
|
||||
id: text("id").primaryKey().$defaultFn(createShareId),
|
||||
content: text("content").notNull(),
|
||||
description: text("description"),
|
||||
imageCdnId: text("image_cdn_id").references(() => cdn.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
source: text("source").$type<ShareSource>().notNull(),
|
||||
authorId: text("author_id").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
authorDiscordId: text("author_discord_id").notNull(),
|
||||
authorName: text("author_name").notNull(),
|
||||
authorAvatarUrl: text("author_avatar_url"),
|
||||
createdAt: timestamp("created_at", { mode: "date", withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(share) => [
|
||||
index("share_text_author_created_idx").on(
|
||||
share.authorDiscordId,
|
||||
share.createdAt
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
export const shareComments = shareSchema.table(
|
||||
"comment",
|
||||
{
|
||||
id: text("id")
|
||||
.primaryKey()
|
||||
.$defaultFn(() => uuidv7()),
|
||||
shareId: text("share_id")
|
||||
.notNull()
|
||||
.references(() => shareTexts.id, { onDelete: "cascade" }),
|
||||
body: text("body").notNull(),
|
||||
authorId: text("author_id").references(() => users.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
authorDiscordId: text("author_discord_id").notNull(),
|
||||
authorName: text("author_name").notNull(),
|
||||
authorAvatarUrl: text("author_avatar_url"),
|
||||
createdAt: timestamp("created_at", { mode: "date", withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
},
|
||||
(comment) => [
|
||||
index("share_comment_share_created_id_idx").on(
|
||||
comment.shareId,
|
||||
comment.createdAt,
|
||||
comment.id
|
||||
),
|
||||
]
|
||||
);
|
||||
+1
-1
@@ -7,5 +7,5 @@ export default defineConfig({
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
schemaFilter: ["public", "account", "form"],
|
||||
schemaFilter: ["public", "account", "form", "share"],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
CREATE TABLE IF NOT EXISTS "public"."video_upload" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"description" text DEFAULT '' NOT NULL,
|
||||
"video_path" text NOT NULL,
|
||||
"thumbnail_path" text,
|
||||
"original_filename" text NOT NULL,
|
||||
"mime_type" text NOT NULL,
|
||||
"size" bigint NOT NULL,
|
||||
"scheduled_at" timestamptz,
|
||||
"status" text DEFAULT 'draft' NOT NULL,
|
||||
"youtube_settings" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"tiktok_settings" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
"created_by_discord_id" text NOT NULL,
|
||||
"created_at" timestamptz DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamptz DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "video_upload_status_scheduled_idx" ON "public"."video_upload" ("status", "scheduled_at");
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "public"."video_upload_job" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"upload_id" text NOT NULL REFERENCES "public"."video_upload"("id") ON DELETE cascade,
|
||||
"platform" text NOT NULL,
|
||||
"status" text DEFAULT 'pending' NOT NULL,
|
||||
"external_id" text,
|
||||
"external_url" text,
|
||||
"error" text,
|
||||
"attempts" integer DEFAULT 0 NOT NULL,
|
||||
"started_at" timestamptz,
|
||||
"completed_at" timestamptz,
|
||||
"created_at" timestamptz DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "video_upload_job_upload_platform_idx" ON "public"."video_upload_job" ("upload_id", "platform");
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE SCHEMA "share";
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "share"."comment" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"share_id" text NOT NULL,
|
||||
"body" text NOT NULL,
|
||||
"author_id" text,
|
||||
"author_discord_id" text NOT NULL,
|
||||
"author_name" text NOT NULL,
|
||||
"author_avatar_url" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "share"."text" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"description" text,
|
||||
"image_cdn_id" text,
|
||||
"source" text NOT NULL,
|
||||
"author_id" text,
|
||||
"author_discord_id" text NOT NULL,
|
||||
"author_name" text NOT NULL,
|
||||
"author_avatar_url" text,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "share"."comment" ADD CONSTRAINT "comment_share_id_text_id_fk" FOREIGN KEY ("share_id") REFERENCES "share"."text"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "share"."comment" ADD CONSTRAINT "comment_author_id_user_id_fk" FOREIGN KEY ("author_id") REFERENCES "account"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "share"."text" ADD CONSTRAINT "text_image_cdn_id_cdn_id_fk" FOREIGN KEY ("image_cdn_id") REFERENCES "public"."cdn"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "share"."text" ADD CONSTRAINT "text_author_id_user_id_fk" FOREIGN KEY ("author_id") REFERENCES "account"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "share_comment_share_created_id_idx" ON "share"."comment" USING btree ("share_id","created_at","id");--> statement-breakpoint
|
||||
CREATE INDEX "share_text_author_created_idx" ON "share"."text" USING btree ("author_discord_id","created_at");--> statement-breakpoint
|
||||
UPDATE "account"."user" AS "user"
|
||||
SET "discord_id" = "account"."providerAccountId"
|
||||
FROM "account"."account" AS "account"
|
||||
WHERE "account"."provider" = 'discord'
|
||||
AND "account"."userId" = "user"."id"
|
||||
AND "user"."discord_id" IS NULL;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "user_discord_id_unique" ON "account"."user" USING btree ("discord_id");
|
||||
@@ -0,0 +1,941 @@
|
||||
{
|
||||
"id": "a15effd0-7081-48ba-a1c9-fa11fed9a810",
|
||||
"prevId": "94182237-a4fa-4101-b263-2689df4a2fad",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"account.account": {
|
||||
"name": "account",
|
||||
"schema": "account",
|
||||
"columns": {
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider": {
|
||||
"name": "provider",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"providerAccountId": {
|
||||
"name": "providerAccountId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"token_type": {
|
||||
"name": "token_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"session_state": {
|
||||
"name": "session_state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_userId_user_id_fk": {
|
||||
"name": "account_userId_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"schemaTo": "account",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"account_provider_providerAccountId_pk": {
|
||||
"name": "account_provider_providerAccountId_pk",
|
||||
"columns": [
|
||||
"provider",
|
||||
"providerAccountId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"form.answer": {
|
||||
"name": "answer",
|
||||
"schema": "form",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"submission_id": {
|
||||
"name": "submission_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"question_id": {
|
||||
"name": "question_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"image_cdn_id": {
|
||||
"name": "image_cdn_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"answer_submission_id_submission_id_fk": {
|
||||
"name": "answer_submission_id_submission_id_fk",
|
||||
"tableFrom": "answer",
|
||||
"tableTo": "submission",
|
||||
"schemaTo": "form",
|
||||
"columnsFrom": [
|
||||
"submission_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"answer_question_id_question_id_fk": {
|
||||
"name": "answer_question_id_question_id_fk",
|
||||
"tableFrom": "answer",
|
||||
"tableTo": "question",
|
||||
"schemaTo": "form",
|
||||
"columnsFrom": [
|
||||
"question_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"answer_image_cdn_id_cdn_id_fk": {
|
||||
"name": "answer_image_cdn_id_cdn_id_fk",
|
||||
"tableFrom": "answer",
|
||||
"tableTo": "cdn",
|
||||
"columnsFrom": [
|
||||
"image_cdn_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "cascade"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.cdn": {
|
||||
"name": "cdn",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"data": {
|
||||
"name": "data",
|
||||
"type": "bytea",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"size": {
|
||||
"name": "size",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"owner_discord_id": {
|
||||
"name": "owner_discord_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"form.form": {
|
||||
"name": "form",
|
||||
"schema": "form",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"is_open": {
|
||||
"name": "is_open",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"closes_at": {
|
||||
"name": "closes_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"discord_webhook_url": {
|
||||
"name": "discord_webhook_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"discord_webhook_template": {
|
||||
"name": "discord_webhook_template",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"discord_webhook_update_template": {
|
||||
"name": "discord_webhook_update_template",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"allowed_roles": {
|
||||
"name": "allowed_roles",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'[]'::jsonb"
|
||||
},
|
||||
"denied_roles": {
|
||||
"name": "denied_roles",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'[]'::jsonb"
|
||||
},
|
||||
"allow_multiple_submissions": {
|
||||
"name": "allow_multiple_submissions",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"form.question": {
|
||||
"name": "question",
|
||||
"schema": "form",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"form_id": {
|
||||
"name": "form_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'text'"
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"display_order": {
|
||||
"name": "display_order",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"required": {
|
||||
"name": "required",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"options": {
|
||||
"name": "options",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'[]'::jsonb"
|
||||
},
|
||||
"allow_other": {
|
||||
"name": "allow_other",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image_answer_mode": {
|
||||
"name": "image_answer_mode",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'none'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"question_form_id_form_id_fk": {
|
||||
"name": "question_form_id_form_id_fk",
|
||||
"tableFrom": "question",
|
||||
"tableTo": "form",
|
||||
"schemaTo": "form",
|
||||
"columnsFrom": [
|
||||
"form_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"account.session": {
|
||||
"name": "session",
|
||||
"schema": "account",
|
||||
"columns": {
|
||||
"sessionToken": {
|
||||
"name": "sessionToken",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_userId_user_id_fk": {
|
||||
"name": "session_userId_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"schemaTo": "account",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"form.submission": {
|
||||
"name": "submission",
|
||||
"schema": "form",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"form_id": {
|
||||
"name": "form_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_discord_id": {
|
||||
"name": "user_discord_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_name": {
|
||||
"name": "user_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"admin_note": {
|
||||
"name": "admin_note",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"submitted_at": {
|
||||
"name": "submitted_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"edit_history": {
|
||||
"name": "edit_history",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'[]'::jsonb"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"submission_form_submitted_at_id_idx": {
|
||||
"name": "submission_form_submitted_at_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "form_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "submitted_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"submission_form_id_form_id_fk": {
|
||||
"name": "submission_form_id_form_id_fk",
|
||||
"tableFrom": "submission",
|
||||
"tableTo": "form",
|
||||
"schemaTo": "form",
|
||||
"columnsFrom": [
|
||||
"form_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"account.user": {
|
||||
"name": "user",
|
||||
"schema": "account",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"emailVerified": {
|
||||
"name": "emailVerified",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"discord_id": {
|
||||
"name": "discord_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"account.verificationToken": {
|
||||
"name": "verificationToken",
|
||||
"schema": "account",
|
||||
"columns": {
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires": {
|
||||
"name": "expires",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"verificationToken_identifier_token_pk": {
|
||||
"name": "verificationToken_identifier_token_pk",
|
||||
"columns": [
|
||||
"identifier",
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.video_upload_job": {
|
||||
"name": "video_upload_job",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"upload_id": {
|
||||
"name": "upload_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"platform": {
|
||||
"name": "platform",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'pending'"
|
||||
},
|
||||
"external_id": {
|
||||
"name": "external_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"external_url": {
|
||||
"name": "external_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"error": {
|
||||
"name": "error",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"attempts": {
|
||||
"name": "attempts",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 0
|
||||
},
|
||||
"started_at": {
|
||||
"name": "started_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"video_upload_job_upload_platform_idx": {
|
||||
"name": "video_upload_job_upload_platform_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "upload_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "platform",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"video_upload_job_upload_id_video_upload_id_fk": {
|
||||
"name": "video_upload_job_upload_id_video_upload_id_fk",
|
||||
"tableFrom": "video_upload_job",
|
||||
"tableTo": "video_upload",
|
||||
"columnsFrom": [
|
||||
"upload_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.video_upload": {
|
||||
"name": "video_upload",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "''"
|
||||
},
|
||||
"video_path": {
|
||||
"name": "video_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"thumbnail_path": {
|
||||
"name": "thumbnail_path",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"original_filename": {
|
||||
"name": "original_filename",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"mime_type": {
|
||||
"name": "mime_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"size": {
|
||||
"name": "size",
|
||||
"type": "bigint",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"scheduled_at": {
|
||||
"name": "scheduled_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'draft'"
|
||||
},
|
||||
"youtube_settings": {
|
||||
"name": "youtube_settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"tiktok_settings": {
|
||||
"name": "tiktok_settings",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::jsonb"
|
||||
},
|
||||
"created_by_discord_id": {
|
||||
"name": "created_by_discord_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"video_upload_status_scheduled_idx": {
|
||||
"name": "video_upload_status_scheduled_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "status",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
},
|
||||
{
|
||||
"expression": "scheduled_at",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {
|
||||
"account": "account",
|
||||
"form": "form"
|
||||
},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,13 @@
|
||||
"when": 1785435766277,
|
||||
"tag": "0017_friendly_video_upload",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1786816029107,
|
||||
"tag": "0018_authenticated_share",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -5,10 +5,33 @@ import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import { db } from "@/db";
|
||||
import { accounts, sessions, users, verificationTokens } from "@/db/schema";
|
||||
import type { SessionWithDiscord } from "@/lib/auth/auth";
|
||||
import { ensureDiscordUser } from "@/lib/auth/discord-user";
|
||||
import { addDiscordGuildMember } from "@/lib/discord/discord";
|
||||
|
||||
const discordIdCache = new Map<string, string>();
|
||||
|
||||
type DiscordOAuthProfile = {
|
||||
id: string;
|
||||
username: string;
|
||||
global_name?: string | null;
|
||||
discriminator?: string;
|
||||
avatar?: string | null;
|
||||
email?: string | null;
|
||||
};
|
||||
|
||||
function discordAvatarUrl(profile: DiscordOAuthProfile) {
|
||||
if (profile.avatar) {
|
||||
const format = profile.avatar.startsWith("a_") ? "gif" : "png";
|
||||
return `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}?size=128`;
|
||||
}
|
||||
|
||||
const discriminator = Number(profile.discriminator ?? 0);
|
||||
const index = discriminator > 0
|
||||
? discriminator % 5
|
||||
: Number((BigInt(profile.id) >> BigInt(22)) % BigInt(6));
|
||||
return `https://cdn.discordapp.com/embed/avatars/${index}.png`;
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
adapter: DrizzleAdapter(db, {
|
||||
usersTable: users,
|
||||
@@ -25,11 +48,46 @@ export const authOptions: NextAuthOptions = {
|
||||
scope: "identify email guilds.join",
|
||||
},
|
||||
},
|
||||
profile(profile: DiscordOAuthProfile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.global_name?.trim() || profile.username,
|
||||
email: profile.email,
|
||||
image: discordAvatarUrl(profile),
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async signIn({ account }) {
|
||||
async signIn({ account, profile, user }) {
|
||||
if (account?.provider === "discord" && account.providerAccountId && account.access_token) {
|
||||
const discordProfile = profile as DiscordOAuthProfile | undefined;
|
||||
const displayName =
|
||||
discordProfile?.global_name?.trim() ||
|
||||
discordProfile?.username ||
|
||||
user.name ||
|
||||
"Discord user";
|
||||
const avatarUrl = discordProfile
|
||||
? discordAvatarUrl(discordProfile)
|
||||
: user.image ?? null;
|
||||
|
||||
const reconciled = await ensureDiscordUser({
|
||||
discordId: account.providerAccountId,
|
||||
displayName,
|
||||
avatarUrl,
|
||||
email: discordProfile?.email ?? user.email ?? null,
|
||||
account: {
|
||||
type: "oauth",
|
||||
refresh_token: account.refresh_token,
|
||||
access_token: account.access_token,
|
||||
expires_at: account.expires_at,
|
||||
token_type: account.token_type,
|
||||
scope: account.scope,
|
||||
id_token: account.id_token,
|
||||
session_state: account.session_state,
|
||||
},
|
||||
});
|
||||
discordIdCache.set(reconciled.id, account.providerAccountId);
|
||||
await addDiscordGuildMember(account.providerAccountId, account.access_token);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import "server-only";
|
||||
|
||||
import type { AdapterAccount } from "next-auth/adapters";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { accounts, users } from "@/db/schema";
|
||||
|
||||
export type DiscordIdentity = {
|
||||
discordId: string;
|
||||
displayName: string;
|
||||
avatarUrl: string | null;
|
||||
email?: string | null;
|
||||
account?: Partial<AdapterAccount>;
|
||||
};
|
||||
|
||||
function accountUpdate(account: Partial<AdapterAccount> | undefined) {
|
||||
if (!account) return {};
|
||||
return {
|
||||
...(account.type !== undefined ? { type: account.type } : {}),
|
||||
...(account.refresh_token !== undefined
|
||||
? { refresh_token: account.refresh_token }
|
||||
: {}),
|
||||
...(account.access_token !== undefined
|
||||
? { access_token: account.access_token }
|
||||
: {}),
|
||||
...(account.expires_at !== undefined
|
||||
? { expires_at: account.expires_at }
|
||||
: {}),
|
||||
...(account.token_type !== undefined
|
||||
? { token_type: account.token_type }
|
||||
: {}),
|
||||
...(account.scope !== undefined ? { scope: account.scope } : {}),
|
||||
...(account.id_token !== undefined ? { id_token: account.id_token } : {}),
|
||||
...(account.session_state !== undefined
|
||||
? { session_state: account.session_state as string | null }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureDiscordUser(
|
||||
identity: DiscordIdentity
|
||||
): Promise<typeof users.$inferSelect> {
|
||||
const { discordId, displayName, avatarUrl, email, account } = identity;
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const [linked] = await tx
|
||||
.select({ userId: accounts.userId })
|
||||
.from(accounts)
|
||||
.where(
|
||||
and(
|
||||
eq(accounts.provider, "discord"),
|
||||
eq(accounts.providerAccountId, discordId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
let userId = linked?.userId;
|
||||
if (!userId) {
|
||||
const [user] = await tx
|
||||
.insert(users)
|
||||
.values({
|
||||
discordId,
|
||||
name: displayName,
|
||||
image: avatarUrl,
|
||||
...(email !== undefined ? { email } : {}),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: users.discordId,
|
||||
set: {
|
||||
name: displayName,
|
||||
image: avatarUrl,
|
||||
...(email !== undefined ? { email } : {}),
|
||||
},
|
||||
})
|
||||
.returning({ id: users.id });
|
||||
userId = user.id;
|
||||
|
||||
await tx
|
||||
.insert(accounts)
|
||||
.values({
|
||||
userId,
|
||||
provider: "discord",
|
||||
providerAccountId: discordId,
|
||||
type: account?.type ?? "oauth",
|
||||
...accountUpdate(account),
|
||||
})
|
||||
.onConflictDoNothing({
|
||||
target: [accounts.provider, accounts.providerAccountId],
|
||||
});
|
||||
|
||||
const [winner] = await tx
|
||||
.select({ userId: accounts.userId })
|
||||
.from(accounts)
|
||||
.where(
|
||||
and(
|
||||
eq(accounts.provider, "discord"),
|
||||
eq(accounts.providerAccountId, discordId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
if (!winner) throw new Error("Failed to link Discord account");
|
||||
userId = winner.userId;
|
||||
}
|
||||
|
||||
const [user] = await tx
|
||||
.update(users)
|
||||
.set({
|
||||
discordId,
|
||||
name: displayName,
|
||||
image: avatarUrl,
|
||||
...(email !== undefined ? { email } : {}),
|
||||
})
|
||||
.where(eq(users.id, userId))
|
||||
.returning();
|
||||
|
||||
const nextAccount = accountUpdate(account);
|
||||
if (Object.keys(nextAccount).length > 0) {
|
||||
await tx
|
||||
.update(accounts)
|
||||
.set(nextAccount)
|
||||
.where(
|
||||
and(
|
||||
eq(accounts.provider, "discord"),
|
||||
eq(accounts.providerAccountId, discordId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) throw new Error("Failed to reconcile Discord user");
|
||||
return user;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user