diff --git a/backend/dist/index.js b/backend/dist/index.js index 3b763ad..645c086 100644 --- a/backend/dist/index.js +++ b/backend/dist/index.js @@ -28305,6 +28305,7 @@ var usersTable = pgTable("users", { idToken: text("id_token"), role: varchar().notNull().default("member"), internalNotes: text("internal_notes"), + verificationStatus: varchar("verification_status"), tutorialCompleted: boolean("tutorial_completed").notNull().default(false), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull() @@ -28469,7 +28470,8 @@ async function createOrUpdateUser(identity, tokens) { avatar: avatarUrl, accessToken: tokens.access_token, refreshToken: tokens.refresh_token, - idToken: tokens.id_token + idToken: tokens.id_token, + verificationStatus: identity.verification_status }).onConflictDoUpdate({ target: usersTable.sub, set: { @@ -28480,6 +28482,7 @@ async function createOrUpdateUser(identity, tokens) { accessToken: tokens.access_token, refreshToken: tokens.refresh_token, idToken: tokens.id_token, + verificationStatus: identity.verification_status, updatedAt: new Date } }).returning(); @@ -28527,16 +28530,22 @@ async function checkUserEligibility(accessToken) { // src/routes/projects.ts var HACKATIME_API = "https://hackatime.hackclub.com/api/v1"; +var SCRAPS_START_DATE = "2026-02-03"; async function fetchHackatimeHours(slackId, projectName) { try { - const url = `${HACKATIME_API}/users/${encodeURIComponent(slackId)}/projects/details`; + const params = new URLSearchParams({ + features: "projects", + start_date: SCRAPS_START_DATE, + filter_by_project: projectName + }); + const url = `${HACKATIME_API}/users/${encodeURIComponent(slackId)}/stats?${params}`; const response = await fetch(url, { headers: { Accept: "application/json" } }); if (!response.ok) return 0; const data = await response.json(); - const project = data.projects.find((p) => p.name === projectName); + const project = data.data?.projects?.find((p) => p.name === projectName); if (!project) return 0; return Math.round(project.total_seconds / 3600 * 10) / 10; @@ -28556,6 +28565,67 @@ function parseHackatimeProject(hackatimeProject) { }; } var projects = new Elysia({ prefix: "/projects" }); +projects.get("/explore", async ({ query }) => { + const page = parseInt(query.page) || 1; + const limit = Math.min(parseInt(query.limit) || 20, 50); + const offset = (page - 1) * limit; + const search = query.search?.trim() || ""; + const tier = query.tier ? parseInt(query.tier) : null; + const status2 = query.status || null; + const conditions = [ + or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted)), + or(eq(projectsTable.status, "shipped"), eq(projectsTable.status, "in_progress")) + ]; + if (search) { + conditions.push(or(ilike(projectsTable.name, `%${search}%`), ilike(projectsTable.description, `%${search}%`))); + } + if (tier && tier >= 1 && tier <= 4) { + conditions.push(eq(projectsTable.tier, tier)); + } + if (status2 === "shipped" || status2 === "in_progress") { + conditions[1] = eq(projectsTable.status, status2); + } + const whereClause = and(...conditions); + const [projectsList, countResult] = await Promise.all([ + db.select({ + id: projectsTable.id, + name: projectsTable.name, + description: projectsTable.description, + image: projectsTable.image, + hours: projectsTable.hours, + tier: projectsTable.tier, + status: projectsTable.status, + views: projectsTable.views, + userId: projectsTable.userId + }).from(projectsTable).where(whereClause).orderBy(desc(projectsTable.updatedAt)).limit(limit).offset(offset), + db.select({ count: sql`count(*)` }).from(projectsTable).where(whereClause) + ]); + const userIds = [...new Set(projectsList.map((p) => p.userId))]; + let users = []; + if (userIds.length > 0) { + users = await db.select({ id: usersTable.id, username: usersTable.username }).from(usersTable).where(inArray(usersTable.id, userIds)); + } + const total = Number(countResult[0]?.count || 0); + return { + data: projectsList.map((p) => ({ + id: p.id, + name: p.name, + description: p.description.substring(0, 150) + (p.description.length > 150 ? "..." : ""), + image: p.image, + hours: p.hours, + tier: p.tier, + status: p.status, + views: p.views, + username: users.find((u) => u.id === p.userId)?.username || null + })), + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + }; +}); projects.get("/", async ({ headers, query }) => { const user = await getUserFromSession(headers); if (!user) @@ -28745,6 +28815,16 @@ projects.post("/:id/submit", async ({ params, headers }) => { const user = await getUserFromSession(headers); if (!user) return { error: "Unauthorized" }; + if (user.accessToken) { + const meResponse = await fetchUserIdentity(user.accessToken); + if (meResponse) { + const { identity } = meResponse; + await db.update(usersTable).set({ verificationStatus: identity.verification_status }).where(eq(usersTable.id, user.id)); + if (identity.verification_status === "ineligible") { + return { error: "ineligible", redirectTo: "/auth/error?reason=not-eligible" }; + } + } + } const project = await db.select().from(projectsTable).where(and(eq(projectsTable.id, parseInt(params.id)), eq(projectsTable.userId, user.id))).limit(1); if (!project[0]) return { error: "Not found" }; @@ -28808,11 +28888,11 @@ var news = new Elysia({ prefix: "/news" }); news.get("/", async () => { - const items = await db.select().from(newsTable).orderBy(desc(newsTable.createdAt)); + const items = await db.select().from(newsTable).where(eq(newsTable.active, true)).orderBy(desc(newsTable.createdAt)); return items; }); news.get("/latest", async () => { - const items = await db.select().from(newsTable).orderBy(desc(newsTable.createdAt)).limit(1); + const items = await db.select().from(newsTable).where(eq(newsTable.active, true)).orderBy(desc(newsTable.createdAt)).limit(1); return items[0] || null; }); var news_default = news; @@ -28929,10 +29009,25 @@ async function canAfford(userId, cost, txOrDb = db) { return false; return balance >= cost; } - +// src/schemas/user-emails.ts +var userEmailsTable = pgTable("user_emails", { + id: serial().primaryKey(), + email: varchar().notNull(), + createdAt: timestamp("created_at").defaultNow().notNull() +}); // src/routes/auth.ts var FRONTEND_URL = config.frontendUrl; var authRoutes = new Elysia({ prefix: "/auth" }); +authRoutes.post("/collect-email", async ({ body }) => { + const { email } = body; + console.log("[AUTH] Collecting email:", email); + await db.insert(userEmailsTable).values({ email }); + return { success: true }; +}, { + body: t.Object({ + email: t.String({ format: "email" }) + }) +}); authRoutes.get("/login", ({ redirect: redirect2 }) => { console.log("[AUTH] Login initiated"); return redirect2(getAuthorizationUrl()); @@ -28964,10 +29059,20 @@ authRoutes.get("/callback", async ({ query, redirect: redirect2, cookie }) => { verificationStatus: identity.verification_status }); const user = await createOrUpdateUser(identity, tokens); + await db.delete(userEmailsTable).where(eq(userEmailsTable.email, identity.primary_email)); + console.log("[AUTH] Deleted collected email:", identity.primary_email); if (user.role === "banned") { console.log("[AUTH] Banned user attempted login:", { userId: user.id, username: user.username }); return redirect2("https://fraud.land"); } + if (identity.verification_status === "needs_submission") { + console.log("[AUTH] User needs to verify identity:", { userId: user.id }); + return redirect2(`${FRONTEND_URL}/auth/error?reason=needs-verification`); + } + if (identity.verification_status === "ineligible") { + console.log("[AUTH] User is ineligible:", { userId: user.id }); + return redirect2(`${FRONTEND_URL}/auth/error?reason=not-eligible`); + } const sessionToken = await createSession(user.id); console.log("[AUTH] User authenticated successfully:", { userId: user.id, username: user.username }); cookie.session.set({ @@ -29058,10 +29163,15 @@ user.post("/complete-tutorial", async ({ headers }) => { if (userData.tutorialCompleted) { return { success: true, alreadyCompleted: true }; } + const existingBonus = await db.select({ id: userBonusesTable.id }).from(userBonusesTable).where(and(eq(userBonusesTable.userId, userData.id), eq(userBonusesTable.reason, "tutorial_completion"))).limit(1); + if (existingBonus.length > 0) { + await db.update(usersTable).set({ tutorialCompleted: true, updatedAt: new Date }).where(eq(usersTable.id, userData.id)); + return { success: true, alreadyCompleted: true }; + } await db.update(usersTable).set({ tutorialCompleted: true, updatedAt: new Date }).where(eq(usersTable.id, userData.id)); await db.insert(userBonusesTable).values({ userId: userData.id, - type: "tutorial_bonus", + reason: "tutorial_completion", amount: 10 }); return { success: true, bonusAwarded: 10 }; @@ -29805,6 +29915,7 @@ var leaderboard_default = leaderboard; // src/routes/hackatime.ts var HACKATIME_API2 = "https://hackatime.hackclub.com/api/v1"; +var SCRAPS_START_DATE2 = "2026-02-03"; var hackatime = new Elysia({ prefix: "/hackatime" }); hackatime.get("/projects", async ({ headers }) => { const user2 = await getUserFromSession(headers); @@ -29814,29 +29925,39 @@ hackatime.get("/projects", async ({ headers }) => { console.log("[HACKATIME] No slackId found for user:", user2.id); return { error: "No Slack ID found for user", projects: [] }; } - const url = `${HACKATIME_API2}/users/${encodeURIComponent(user2.slackId)}/projects/details`; - console.log("[HACKATIME] Fetching projects:", { userId: user2.id, slackId: user2.slackId, url }); + const statsParams = new URLSearchParams({ + features: "projects", + start_date: SCRAPS_START_DATE2 + }); + const statsUrl = `${HACKATIME_API2}/users/${encodeURIComponent(user2.slackId)}/stats?${statsParams}`; + const detailsUrl = `${HACKATIME_API2}/users/${encodeURIComponent(user2.slackId)}/projects/details`; + console.log("[HACKATIME] Fetching projects:", { userId: user2.id, slackId: user2.slackId, statsUrl }); try { - const response = await fetch(url, { - headers: { - Accept: "application/json" - } - }); - if (!response.ok) { - const errorText = await response.text(); - console.log("[HACKATIME] API error:", { status: response.status, body: errorText }); + const [statsResponse, detailsResponse] = await Promise.all([ + fetch(statsUrl, { headers: { Accept: "application/json" } }), + fetch(detailsUrl, { headers: { Accept: "application/json" } }) + ]); + if (!statsResponse.ok) { + const errorText = await statsResponse.text(); + console.log("[HACKATIME] Stats API error:", { status: statsResponse.status, body: errorText }); return { projects: [] }; } - const data = await response.json(); - console.log("[HACKATIME] Projects fetched:", data.projects?.length || 0); + const statsData = await statsResponse.json(); + const detailsData = detailsResponse.ok ? await detailsResponse.json() : { projects: [] }; + const projects2 = statsData.data?.projects || []; + console.log("[HACKATIME] Projects fetched:", projects2.length); + const detailsMap = new Map(detailsData.projects?.map((p) => [p.name, p]) || []); return { slackId: user2.slackId, - projects: data.projects.map((p) => ({ - name: p.name, - hours: Math.round(p.total_seconds / 3600 * 10) / 10, - repoUrl: p.repo_url, - languages: p.languages - })) + projects: projects2.map((p) => { + const details = detailsMap.get(p.name); + return { + name: p.name, + hours: Math.round(p.total_seconds / 3600 * 10) / 10, + repoUrl: details?.repo_url || null, + languages: details?.languages || [] + }; + }) }; } catch (error) { console.error("[HACKATIME] Error fetching projects:", error); @@ -29915,6 +30036,27 @@ async function requireAdmin(headers) { return null; return user2; } +admin.get("/stats", async ({ headers, status: status2 }) => { + const user2 = await requireReviewer(headers); + if (!user2) { + return status2(401, { error: "Unauthorized" }); + } + const [usersCount, projectsCount, totalHoursResult] = await Promise.all([ + db.select({ count: sql`count(*)` }).from(usersTable), + db.select({ count: sql`count(*)` }).from(projectsTable).where(or(eq(projectsTable.deleted, 0), sql`${projectsTable.deleted} IS NULL`)), + db.select({ total: sql`COALESCE(SUM(COALESCE(${projectsTable.hoursOverride}, ${projectsTable.hours})), 0)` }).from(projectsTable).where(and(eq(projectsTable.status, "shipped"), or(eq(projectsTable.deleted, 0), sql`${projectsTable.deleted} IS NULL`))) + ]); + const totalUsers = Number(usersCount[0]?.count || 0); + const totalProjects = Number(projectsCount[0]?.count || 0); + const totalHours = Number(totalHoursResult[0]?.total || 0); + const weightedGrants = Math.round(totalHours / 10 * 100) / 100; + return { + totalUsers, + totalProjects, + totalHours: Math.round(totalHours * 10) / 10, + weightedGrants + }; +}); admin.get("/users", async ({ headers, query, status: status2 }) => { try { const user2 = await requireReviewer(headers); @@ -30208,6 +30350,9 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => { const project = await db.select().from(projectsTable).where(eq(projectsTable.id, projectId)).limit(1); if (!project[0]) return { error: "Project not found" }; + if (hoursOverride !== undefined && hoursOverride > (project[0].hours ?? 0)) { + return { error: `Hours override (${hoursOverride}) cannot exceed project hours (${project[0].hours})` }; + } if (project[0].deleted) { return { error: "Cannot review a deleted project" }; } diff --git a/backend/drizzle/0000_giant_colonel_america.sql b/backend/drizzle/0000_giant_colonel_america.sql new file mode 100644 index 0000000..013c9ea --- /dev/null +++ b/backend/drizzle/0000_giant_colonel_america.sql @@ -0,0 +1,175 @@ +CREATE TABLE "activity" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "activity_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "project_id" integer, + "action" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "news" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "news_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "title" varchar NOT NULL, + "content" varchar NOT NULL, + "active" boolean DEFAULT true NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "projects" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "projects_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "name" varchar NOT NULL, + "description" varchar NOT NULL, + "image" text, + "github_url" varchar, + "playable_url" varchar, + "hackatime_project" varchar, + "hours" real DEFAULT 0, + "hours_override" real, + "tier" integer DEFAULT 1 NOT NULL, + "tier_override" integer, + "status" varchar DEFAULT 'in_progress' NOT NULL, + "deleted" integer DEFAULT 0, + "scraps_awarded" integer DEFAULT 0 NOT NULL, + "views" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "refinery_orders" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "refinery_orders_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "shop_item_id" integer NOT NULL, + "cost" integer NOT NULL, + "boost_amount" integer DEFAULT 1 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "reviews" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "reviews_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "project_id" integer NOT NULL, + "reviewer_id" integer NOT NULL, + "action" varchar NOT NULL, + "feedback_for_author" text NOT NULL, + "internal_justification" text, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "token" varchar PRIMARY KEY NOT NULL, + "user_id" integer NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "shop_hearts" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "shop_hearts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "shop_item_id" integer NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "shop_hearts_user_id_shop_item_id_unique" UNIQUE("user_id","shop_item_id") +); +--> statement-breakpoint +CREATE TABLE "shop_items" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "shop_items_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "name" varchar NOT NULL, + "image" varchar NOT NULL, + "description" varchar NOT NULL, + "price" integer NOT NULL, + "category" varchar NOT NULL, + "count" integer DEFAULT 0 NOT NULL, + "base_probability" integer DEFAULT 50 NOT NULL, + "base_upgrade_cost" integer DEFAULT 10 NOT NULL, + "cost_multiplier" integer DEFAULT 115 NOT NULL, + "boost_amount" integer DEFAULT 1 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "shop_orders" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "shop_orders_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "shop_item_id" integer NOT NULL, + "quantity" integer DEFAULT 1 NOT NULL, + "price_per_item" integer NOT NULL, + "total_price" integer NOT NULL, + "status" varchar DEFAULT 'pending' NOT NULL, + "order_type" varchar DEFAULT 'purchase' NOT NULL, + "shipping_address" text, + "notes" text, + "is_fulfilled" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "shop_penalties" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "shop_penalties_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "shop_item_id" integer NOT NULL, + "probability_multiplier" integer DEFAULT 100 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "shop_penalties_user_id_shop_item_id_unique" UNIQUE("user_id","shop_item_id") +); +--> statement-breakpoint +CREATE TABLE "shop_rolls" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "shop_rolls_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "shop_item_id" integer NOT NULL, + "rolled" integer NOT NULL, + "threshold" integer NOT NULL, + "won" boolean NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "user_emails" ( + "id" serial PRIMARY KEY NOT NULL, + "email" varchar NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "users_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "sub" varchar NOT NULL, + "slack_id" varchar, + "username" varchar, + "email" varchar NOT NULL, + "avatar" varchar, + "access_token" text, + "refresh_token" text, + "id_token" text, + "role" varchar DEFAULT 'member' NOT NULL, + "internal_notes" text, + "tutorial_completed" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "users_sub_unique" UNIQUE("sub") +); +--> statement-breakpoint +CREATE TABLE "user_bonuses" ( + "id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "user_bonuses_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), + "user_id" integer NOT NULL, + "amount" integer NOT NULL, + "reason" text NOT NULL, + "given_by" integer, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "activity" ADD CONSTRAINT "activity_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "activity" ADD CONSTRAINT "activity_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "refinery_orders" ADD CONSTRAINT "refinery_orders_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "refinery_orders" ADD CONSTRAINT "refinery_orders_shop_item_id_shop_items_id_fk" FOREIGN KEY ("shop_item_id") REFERENCES "public"."shop_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_reviewer_id_users_id_fk" FOREIGN KEY ("reviewer_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_hearts" ADD CONSTRAINT "shop_hearts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_hearts" ADD CONSTRAINT "shop_hearts_shop_item_id_shop_items_id_fk" FOREIGN KEY ("shop_item_id") REFERENCES "public"."shop_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_orders" ADD CONSTRAINT "shop_orders_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_orders" ADD CONSTRAINT "shop_orders_shop_item_id_shop_items_id_fk" FOREIGN KEY ("shop_item_id") REFERENCES "public"."shop_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_penalties" ADD CONSTRAINT "shop_penalties_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_penalties" ADD CONSTRAINT "shop_penalties_shop_item_id_shop_items_id_fk" FOREIGN KEY ("shop_item_id") REFERENCES "public"."shop_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_rolls" ADD CONSTRAINT "shop_rolls_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "shop_rolls" ADD CONSTRAINT "shop_rolls_shop_item_id_shop_items_id_fk" FOREIGN KEY ("shop_item_id") REFERENCES "public"."shop_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_bonuses" ADD CONSTRAINT "user_bonuses_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "user_bonuses" ADD CONSTRAINT "user_bonuses_given_by_users_id_fk" FOREIGN KEY ("given_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/backend/drizzle/0001_short_domino.sql b/backend/drizzle/0001_short_domino.sql new file mode 100644 index 0000000..b9dd60a --- /dev/null +++ b/backend/drizzle/0001_short_domino.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "verification_status" varchar; \ No newline at end of file diff --git a/backend/drizzle/meta/0000_snapshot.json b/backend/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..729c21f --- /dev/null +++ b/backend/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1315 @@ +{ + "id": "d83c7cea-877a-4a82-826e-936dea19a62d", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity": { + "name": "activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "activity_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "activity_user_id_users_id_fk": { + "name": "activity_user_id_users_id_fk", + "tableFrom": "activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_project_id_projects_id_fk": { + "name": "activity_project_id_projects_id_fk", + "tableFrom": "activity", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.news": { + "name": "news", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "news_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "projects_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "playable_url": { + "name": "playable_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hackatime_project": { + "name": "hackatime_project", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "hours_override": { + "name": "hours_override", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "tier_override": { + "name": "tier_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "scraps_awarded": { + "name": "scraps_awarded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_user_id_users_id_fk": { + "name": "projects_user_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refinery_orders": { + "name": "refinery_orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "refinery_orders_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "boost_amount": { + "name": "boost_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "refinery_orders_user_id_users_id_fk": { + "name": "refinery_orders_user_id_users_id_fk", + "tableFrom": "refinery_orders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "refinery_orders_shop_item_id_shop_items_id_fk": { + "name": "refinery_orders_shop_item_id_shop_items_id_fk", + "tableFrom": "refinery_orders", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviews": { + "name": "reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "reviews_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reviewer_id": { + "name": "reviewer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "feedback_for_author": { + "name": "feedback_for_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_justification": { + "name": "internal_justification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reviews_project_id_projects_id_fk": { + "name": "reviews_project_id_projects_id_fk", + "tableFrom": "reviews", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reviews_reviewer_id_users_id_fk": { + "name": "reviews_reviewer_id_users_id_fk", + "tableFrom": "reviews", + "tableTo": "users", + "columnsFrom": [ + "reviewer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_hearts": { + "name": "shop_hearts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_hearts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_hearts_user_id_users_id_fk": { + "name": "shop_hearts_user_id_users_id_fk", + "tableFrom": "shop_hearts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_hearts_shop_item_id_shop_items_id_fk": { + "name": "shop_hearts_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_hearts", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shop_hearts_user_id_shop_item_id_unique": { + "name": "shop_hearts_user_id_shop_item_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "shop_item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_items": { + "name": "shop_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_items_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "base_probability": { + "name": "base_probability", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "base_upgrade_cost": { + "name": "base_upgrade_cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 115 + }, + "boost_amount": { + "name": "boost_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_orders": { + "name": "shop_orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_orders_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "price_per_item": { + "name": "price_per_item", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_price": { + "name": "total_price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "order_type": { + "name": "order_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'purchase'" + }, + "shipping_address": { + "name": "shipping_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_fulfilled": { + "name": "is_fulfilled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_orders_user_id_users_id_fk": { + "name": "shop_orders_user_id_users_id_fk", + "tableFrom": "shop_orders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_orders_shop_item_id_shop_items_id_fk": { + "name": "shop_orders_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_orders", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_penalties": { + "name": "shop_penalties", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_penalties_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "probability_multiplier": { + "name": "probability_multiplier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_penalties_user_id_users_id_fk": { + "name": "shop_penalties_user_id_users_id_fk", + "tableFrom": "shop_penalties", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_penalties_shop_item_id_shop_items_id_fk": { + "name": "shop_penalties_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_penalties", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shop_penalties_user_id_shop_item_id_unique": { + "name": "shop_penalties_user_id_shop_item_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "shop_item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_rolls": { + "name": "shop_rolls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_rolls_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rolled": { + "name": "rolled", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "won": { + "name": "won", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_rolls_user_id_users_id_fk": { + "name": "shop_rolls_user_id_users_id_fk", + "tableFrom": "shop_rolls", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_rolls_shop_item_id_shop_items_id_fk": { + "name": "shop_rolls_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_rolls", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_emails": { + "name": "user_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "sub": { + "name": "sub", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slack_id": { + "name": "slack_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "internal_notes": { + "name": "internal_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tutorial_completed": { + "name": "tutorial_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_sub_unique": { + "name": "users_sub_unique", + "nullsNotDistinct": false, + "columns": [ + "sub" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_bonuses": { + "name": "user_bonuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_bonuses_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_by": { + "name": "given_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_bonuses_user_id_users_id_fk": { + "name": "user_bonuses_user_id_users_id_fk", + "tableFrom": "user_bonuses", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_bonuses_given_by_users_id_fk": { + "name": "user_bonuses_given_by_users_id_fk", + "tableFrom": "user_bonuses", + "tableTo": "users", + "columnsFrom": [ + "given_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/0001_snapshot.json b/backend/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..4590b2b --- /dev/null +++ b/backend/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1321 @@ +{ + "id": "11af80b8-0b2d-4dd5-9ecc-a6c3907d66e7", + "prevId": "d83c7cea-877a-4a82-826e-936dea19a62d", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity": { + "name": "activity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "activity_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "activity_user_id_users_id_fk": { + "name": "activity_user_id_users_id_fk", + "tableFrom": "activity", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_project_id_projects_id_fk": { + "name": "activity_project_id_projects_id_fk", + "tableFrom": "activity", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.news": { + "name": "news", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "news_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "projects_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_url": { + "name": "github_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "playable_url": { + "name": "playable_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hackatime_project": { + "name": "hackatime_project", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "hours": { + "name": "hours", + "type": "real", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "hours_override": { + "name": "hours_override", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "tier": { + "name": "tier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "tier_override": { + "name": "tier_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'in_progress'" + }, + "deleted": { + "name": "deleted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "scraps_awarded": { + "name": "scraps_awarded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "projects_user_id_users_id_fk": { + "name": "projects_user_id_users_id_fk", + "tableFrom": "projects", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refinery_orders": { + "name": "refinery_orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "refinery_orders_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "boost_amount": { + "name": "boost_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "refinery_orders_user_id_users_id_fk": { + "name": "refinery_orders_user_id_users_id_fk", + "tableFrom": "refinery_orders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "refinery_orders_shop_item_id_shop_items_id_fk": { + "name": "refinery_orders_shop_item_id_shop_items_id_fk", + "tableFrom": "refinery_orders", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reviews": { + "name": "reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "reviews_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "project_id": { + "name": "project_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reviewer_id": { + "name": "reviewer_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "feedback_for_author": { + "name": "feedback_for_author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "internal_justification": { + "name": "internal_justification", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reviews_project_id_projects_id_fk": { + "name": "reviews_project_id_projects_id_fk", + "tableFrom": "reviews", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reviews_reviewer_id_users_id_fk": { + "name": "reviews_reviewer_id_users_id_fk", + "tableFrom": "reviews", + "tableTo": "users", + "columnsFrom": [ + "reviewer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "token": { + "name": "token", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_hearts": { + "name": "shop_hearts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_hearts_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_hearts_user_id_users_id_fk": { + "name": "shop_hearts_user_id_users_id_fk", + "tableFrom": "shop_hearts", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_hearts_shop_item_id_shop_items_id_fk": { + "name": "shop_hearts_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_hearts", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shop_hearts_user_id_shop_item_id_unique": { + "name": "shop_hearts_user_id_shop_item_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "shop_item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_items": { + "name": "shop_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_items_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "price": { + "name": "price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "base_probability": { + "name": "base_probability", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "base_upgrade_cost": { + "name": "base_upgrade_cost", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "cost_multiplier": { + "name": "cost_multiplier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 115 + }, + "boost_amount": { + "name": "boost_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_orders": { + "name": "shop_orders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_orders_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "price_per_item": { + "name": "price_per_item", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "total_price": { + "name": "total_price", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "order_type": { + "name": "order_type", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'purchase'" + }, + "shipping_address": { + "name": "shipping_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_fulfilled": { + "name": "is_fulfilled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_orders_user_id_users_id_fk": { + "name": "shop_orders_user_id_users_id_fk", + "tableFrom": "shop_orders", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_orders_shop_item_id_shop_items_id_fk": { + "name": "shop_orders_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_orders", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_penalties": { + "name": "shop_penalties", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_penalties_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "probability_multiplier": { + "name": "probability_multiplier", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_penalties_user_id_users_id_fk": { + "name": "shop_penalties_user_id_users_id_fk", + "tableFrom": "shop_penalties", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_penalties_shop_item_id_shop_items_id_fk": { + "name": "shop_penalties_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_penalties", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "shop_penalties_user_id_shop_item_id_unique": { + "name": "shop_penalties_user_id_shop_item_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id", + "shop_item_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.shop_rolls": { + "name": "shop_rolls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "shop_rolls_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "shop_item_id": { + "name": "shop_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "rolled": { + "name": "rolled", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "threshold": { + "name": "threshold", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "won": { + "name": "won", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "shop_rolls_user_id_users_id_fk": { + "name": "shop_rolls_user_id_users_id_fk", + "tableFrom": "shop_rolls", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "shop_rolls_shop_item_id_shop_items_id_fk": { + "name": "shop_rolls_shop_item_id_shop_items_id_fk", + "tableFrom": "shop_rolls", + "tableTo": "shop_items", + "columnsFrom": [ + "shop_item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_emails": { + "name": "user_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "sub": { + "name": "sub", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "slack_id": { + "name": "slack_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "varchar", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "internal_notes": { + "name": "internal_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verification_status": { + "name": "verification_status", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "tutorial_completed": { + "name": "tutorial_completed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_sub_unique": { + "name": "users_sub_unique", + "nullsNotDistinct": false, + "columns": [ + "sub" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_bonuses": { + "name": "user_bonuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_bonuses_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_by": { + "name": "given_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "user_bonuses_user_id_users_id_fk": { + "name": "user_bonuses_user_id_users_id_fk", + "tableFrom": "user_bonuses", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_bonuses_given_by_users_id_fk": { + "name": "user_bonuses_given_by_users_id_fk", + "tableFrom": "user_bonuses", + "tableTo": "users", + "columnsFrom": [ + "given_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/backend/drizzle/meta/_journal.json b/backend/drizzle/meta/_journal.json new file mode 100644 index 0000000..1c8989d --- /dev/null +++ b/backend/drizzle/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1770241110696, + "tag": "0000_giant_colonel_america", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1770241831758, + "tag": "0001_short_domino", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts index b22906a..a716e2f 100644 --- a/backend/src/lib/auth.ts +++ b/backend/src/lib/auth.ts @@ -89,7 +89,8 @@ export async function fetchUserIdentity(accessToken: string): Promise) { return user } +// Get admin stats (info page) +admin.get('/stats', async ({ headers, status }) => { + const user = await requireReviewer(headers as Record) + if (!user) { + return status(401, { error: 'Unauthorized' }) + } + + const [usersCount, projectsCount, totalHoursResult] = await Promise.all([ + db.select({ count: sql`count(*)` }).from(usersTable), + db.select({ count: sql`count(*)` }) + .from(projectsTable) + .where(or(eq(projectsTable.deleted, 0), sql`${projectsTable.deleted} IS NULL`)), + db.select({ total: sql`COALESCE(SUM(COALESCE(${projectsTable.hoursOverride}, ${projectsTable.hours})), 0)` }) + .from(projectsTable) + .where(and( + eq(projectsTable.status, 'shipped'), + or(eq(projectsTable.deleted, 0), sql`${projectsTable.deleted} IS NULL`) + )) + ]) + + const totalUsers = Number(usersCount[0]?.count || 0) + const totalProjects = Number(projectsCount[0]?.count || 0) + const totalHours = Number(totalHoursResult[0]?.total || 0) + const weightedGrants = Math.round(totalHours / 10 * 100) / 100 + + return { + totalUsers, + totalProjects, + totalHours: Math.round(totalHours * 10) / 10, + weightedGrants + } +}) + // Get all users (reviewers see limited info) admin.get('/users', async ({ headers, query, status }) => { try { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 933669a..2f14ddf 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,4 +1,4 @@ -import { Elysia } from "elysia" +import { Elysia, t } from "elysia" import { getAuthorizationUrl, exchangeCodeForTokens, @@ -10,11 +10,28 @@ import { } from "../lib/auth" import { config } from "../config" import { getUserScrapsBalance } from "../lib/scraps" +import { db } from "../db" +import { userEmailsTable } from "../schemas" +import { eq } from "drizzle-orm" const FRONTEND_URL = config.frontendUrl const authRoutes = new Elysia({ prefix: "/auth" }) +// POST /auth/collect-email - Store email before redirecting to auth +authRoutes.post("/collect-email", async ({ body }) => { + const { email } = body as { email: string } + console.log("[AUTH] Collecting email:", email) + + await db.insert(userEmailsTable).values({ email }) + + return { success: true } +}, { + body: t.Object({ + email: t.String({ format: 'email' }) + }) +}) + // GET /auth/login - Redirect to Hack Club Auth authRoutes.get("/login", ({ redirect }) => { console.log("[AUTH] Login initiated") @@ -53,8 +70,23 @@ authRoutes.get("/callback", async ({ query, redirect, cookie }) => { verificationStatus: identity.verification_status }) + // Check verification status BEFORE creating user + if (identity.verification_status === 'needs_submission') { + console.log("[AUTH] User needs to verify identity") + return redirect(`${FRONTEND_URL}/auth/error?reason=needs-verification`) + } + + if (identity.verification_status === 'ineligible') { + console.log("[AUTH] User is ineligible") + return redirect(`${FRONTEND_URL}/auth/error?reason=not-eligible`) + } + const user = await createOrUpdateUser(identity, tokens) + // Delete the collected email since user completed auth + await db.delete(userEmailsTable).where(eq(userEmailsTable.email, identity.primary_email)) + console.log("[AUTH] Deleted collected email:", identity.primary_email) + if (user.role === 'banned') { console.log("[AUTH] Banned user attempted login:", { userId: user.id, username: user.username }) return redirect('https://fraud.land') diff --git a/backend/src/routes/hackatime.ts b/backend/src/routes/hackatime.ts index aedb08f..f362708 100644 --- a/backend/src/routes/hackatime.ts +++ b/backend/src/routes/hackatime.ts @@ -2,19 +2,28 @@ import { Elysia } from 'elysia' import { getUserFromSession } from '../lib/auth' const HACKATIME_API = 'https://hackatime.hackclub.com/api/v1' +const SCRAPS_START_DATE = '2026-02-03' -interface HackatimeProject { +interface HackatimeStatsProject { + name: string + total_seconds: number +} + +interface HackatimeDetailsProject { name: string total_seconds: number languages: string[] repo_url: string | null - total_heartbeats: number - first_heartbeat: string - last_heartbeat: string } -interface HackatimeResponse { - projects: HackatimeProject[] +interface HackatimeStatsResponse { + data: { + projects: HackatimeStatsProject[] + } +} + +interface HackatimeDetailsResponse { + projects: HackatimeDetailsProject[] } const hackatime = new Elysia({ prefix: '/hackatime' }) @@ -28,33 +37,51 @@ hackatime.get('/projects', async ({ headers }) => { return { error: 'No Slack ID found for user', projects: [] } } - const url = `${HACKATIME_API}/users/${encodeURIComponent(user.slackId)}/projects/details` - console.log('[HACKATIME] Fetching projects:', { userId: user.id, slackId: user.slackId, url }) + const statsParams = new URLSearchParams({ + features: 'projects', + start_date: SCRAPS_START_DATE + }) + const statsUrl = `${HACKATIME_API}/users/${encodeURIComponent(user.slackId)}/stats?${statsParams}` + const detailsUrl = `${HACKATIME_API}/users/${encodeURIComponent(user.slackId)}/projects/details` + console.log('[HACKATIME] Fetching projects:', { userId: user.id, slackId: user.slackId, statsUrl }) try { - const response = await fetch(url, { - headers: { - 'Accept': 'application/json' - } - }) + // Fetch both stats (for hours after start date) and details (for repo URLs and languages) + const [statsResponse, detailsResponse] = await Promise.all([ + fetch(statsUrl, { headers: { 'Accept': 'application/json' } }), + fetch(detailsUrl, { headers: { 'Accept': 'application/json' } }) + ]) - if (!response.ok) { - const errorText = await response.text() - console.log('[HACKATIME] API error:', { status: response.status, body: errorText }) + if (!statsResponse.ok) { + const errorText = await statsResponse.text() + console.log('[HACKATIME] Stats API error:', { status: statsResponse.status, body: errorText }) return { projects: [] } } - const data: HackatimeResponse = await response.json() - console.log('[HACKATIME] Projects fetched:', data.projects?.length || 0) + const statsData: HackatimeStatsResponse = await statsResponse.json() + const detailsData: HackatimeDetailsResponse = detailsResponse.ok + ? await detailsResponse.json() + : { projects: [] } + + const projects = statsData.data?.projects || [] + console.log('[HACKATIME] Projects fetched:', projects.length) + + // Create a map of project details for quick lookup + const detailsMap = new Map( + detailsData.projects?.map(p => [p.name, p]) || [] + ) return { slackId: user.slackId, - projects: data.projects.map((p) => ({ - name: p.name, - hours: Math.round(p.total_seconds / 3600 * 10) / 10, - repoUrl: p.repo_url, - languages: p.languages - })) + projects: projects.map((p) => { + const details = detailsMap.get(p.name) + return { + name: p.name, + hours: Math.round(p.total_seconds / 3600 * 10) / 10, + repoUrl: details?.repo_url || null, + languages: details?.languages || [] + } + }) } } catch (error) { console.error('[HACKATIME] Error fetching projects:', error) diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index b8a6f6b..2b86209 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -5,29 +5,37 @@ import { projectsTable } from '../schemas/projects' import { reviewsTable } from '../schemas/reviews' import { usersTable } from '../schemas/users' import { activityTable } from '../schemas/activity' -import { getUserFromSession } from '../lib/auth' +import { getUserFromSession, fetchUserIdentity } from '../lib/auth' const HACKATIME_API = 'https://hackatime.hackclub.com/api/v1' +const SCRAPS_START_DATE = '2026-02-03' -interface HackatimeProject { +interface HackatimeStatsProject { name: string total_seconds: number } -interface HackatimeResponse { - projects: HackatimeProject[] +interface HackatimeStatsResponse { + data: { + projects: HackatimeStatsProject[] + } } async function fetchHackatimeHours(slackId: string, projectName: string): Promise { try { - const url = `${HACKATIME_API}/users/${encodeURIComponent(slackId)}/projects/details` + const params = new URLSearchParams({ + features: 'projects', + start_date: SCRAPS_START_DATE, + filter_by_project: projectName + }) + const url = `${HACKATIME_API}/users/${encodeURIComponent(slackId)}/stats?${params}` const response = await fetch(url, { headers: { 'Accept': 'application/json' } }) if (!response.ok) return 0 - const data: HackatimeResponse = await response.json() - const project = data.projects.find(p => p.name === projectName) + const data: HackatimeStatsResponse = await response.json() + const project = data.data?.projects?.find(p => p.name === projectName) if (!project) return 0 return Math.round(project.total_seconds / 3600 * 10) / 10 @@ -443,6 +451,22 @@ projects.post("/:id/submit", async ({ params, headers }) => { const user = await getUserFromSession(headers as Record) if (!user) return { error: "Unauthorized" } + // Check verification status before allowing submission + if (user.accessToken) { + const meResponse = await fetchUserIdentity(user.accessToken) + if (meResponse) { + const { identity } = meResponse + // Update stored verification status + await db.update(usersTable) + .set({ verificationStatus: identity.verification_status }) + .where(eq(usersTable.id, user.id)) + + if (identity.verification_status === 'ineligible') { + return { error: "ineligible", redirectTo: "/auth/error?reason=not-eligible" } + } + } + } + const project = await db .select() .from(projectsTable) diff --git a/backend/src/schemas/index.ts b/backend/src/schemas/index.ts index b360045..ec57d1a 100644 --- a/backend/src/schemas/index.ts +++ b/backend/src/schemas/index.ts @@ -5,3 +5,4 @@ export { sessionsTable } from './sessions' export { shopItemsTable, shopHeartsTable, shopOrdersTable, shopRollsTable, refineryOrdersTable, shopPenaltiesTable } from './shop' export { newsTable } from './news' export { activityTable } from './activity' +export { userEmailsTable } from './user-emails' diff --git a/backend/src/schemas/user-emails.ts b/backend/src/schemas/user-emails.ts new file mode 100644 index 0000000..95b0ff7 --- /dev/null +++ b/backend/src/schemas/user-emails.ts @@ -0,0 +1,7 @@ +import { pgTable, varchar, timestamp, serial } from 'drizzle-orm/pg-core' + +export const userEmailsTable = pgTable('user_emails', { + id: serial().primaryKey(), + email: varchar().notNull(), + createdAt: timestamp('created_at').defaultNow().notNull() +}) diff --git a/backend/src/schemas/users.ts b/backend/src/schemas/users.ts index 004d89c..e6e05df 100644 --- a/backend/src/schemas/users.ts +++ b/backend/src/schemas/users.ts @@ -21,6 +21,9 @@ export const usersTable = pgTable('users', { role: varchar().notNull().default('member'), internalNotes: text('internal_notes'), + // verification status from Hack Club Auth + verificationStatus: varchar('verification_status'), + // tutorial status tutorialCompleted: boolean('tutorial_completed').notNull().default(false), diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index 6845bd9..f0debb5 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -17,7 +17,8 @@ ShoppingBag, Newspaper, PackageCheck, - Compass + Compass, + BarChart3 } from '@lucide/svelte' import { logout, getUser, userScrapsStore } from '$lib/auth-client' @@ -169,6 +170,16 @@
{:else} + + + info + + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index d0b333c..38b1404 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -25,6 +25,8 @@ } }) + let hideNavbar = $derived($page.url.pathname.startsWith('/auth/error')) + // Prefetch data on initial load if user is logged in onMount(async () => { user = await getUser() @@ -48,7 +50,9 @@
- + {#if !hideNavbar} + + {/if}
{@render children()}
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 2cc56b7..65693b8 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -5,8 +5,38 @@ import { login } from '$lib/auth-client' import { API_URL } from '$lib/config' - function handleLogin() { - login() + let email = $state('') + let emailError = $state('') + let isSubmitting = $state(false) + + function isValidEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) + } + + async function handleLogin() { + emailError = '' + + if (!email.trim()) { + emailError = 'please enter your email' + return + } + + if (!isValidEmail(email)) { + emailError = 'please enter a valid email' + return + } + + isSubmitting = true + try { + await fetch(`${API_URL}/auth/collect-email`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }) + login() + } catch { + login() + } } interface ShopItem { @@ -138,13 +168,25 @@

- +
+ + {#if emailError} +

{emailError}

+ {/if} + +
diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte new file mode 100644 index 0000000..14585d0 --- /dev/null +++ b/frontend/src/routes/admin/+page.svelte @@ -0,0 +1,95 @@ + + + + admin info - scraps + + +
+

admin info

+ + {#if loading} +
loading stats...
+ {:else if error} +
{error}
+ {:else if stats} +
+
+
+ +
+
+

total users

+

{stats.totalUsers.toLocaleString()}

+
+
+ +
+
+ +
+
+

total projects

+

{stats.totalProjects.toLocaleString()}

+
+
+ +
+
+ +
+
+

total shipped hours

+

{stats.totalHours.toLocaleString()}h

+
+
+ +
+
+ +
+
+

weighted grants

+

{stats.weightedGrants.toLocaleString()}

+

total hours รท 10

+
+
+
+ {/if} +
diff --git a/frontend/src/routes/auth/error/+page.svelte b/frontend/src/routes/auth/error/+page.svelte index ff0bd11..1fece92 100644 --- a/frontend/src/routes/auth/error/+page.svelte +++ b/frontend/src/routes/auth/error/+page.svelte @@ -4,10 +4,16 @@ let reason = $derived(page.url.searchParams.get('reason') || 'unknown') - const errorMessages: Record = { + const errorMessages: Record = { + 'needs-verification': { + title: 'verify your identity', + description: 'you need to verify your identity with hack club auth before you can use scraps. click below to complete verification.', + redirectUrl: 'https://auth.hackclub.com', + redirectText: 'verify with hack club auth' + }, 'not-eligible': { title: 'not eligible for ysws', - description: 'your hack club account is not currently eligible for you ship we ship programs. this might be because your account needs to be verified first.' + description: 'your hack club account is not currently eligible for you ship we ship programs. please ask for help in the hack club slack.' }, 'auth-failed': { title: 'authentication failed', @@ -44,14 +50,25 @@ > go back home
- - get help on slack - + {#if errorInfo.redirectUrl} + + {errorInfo.redirectText} + + {:else} + + get help on slack + + {/if} diff --git a/frontend/src/routes/projects/[id]/submit/+page.svelte b/frontend/src/routes/projects/[id]/submit/+page.svelte index 7e9557f..0809b79 100644 --- a/frontend/src/routes/projects/[id]/submit/+page.svelte +++ b/frontend/src/routes/projects/[id]/submit/+page.svelte @@ -163,9 +163,15 @@ credentials: 'include' }) + const submitData = await submitResponse.json().catch(() => ({})) + + if (submitData.error === 'ineligible' && submitData.redirectTo) { + goto(submitData.redirectTo) + return + } + if (!submitResponse.ok) { - const responseData = await submitResponse.json().catch(() => ({})) - throw new Error(responseData.message || 'Failed to submit project') + throw new Error(submitData.message || 'Failed to submit project') } goto(`/projects/${project.id}`)