refactor(db): finish Drizzle migration for remaining actions and services
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
22234fe102
commit
65b2fbee6a
19 files changed
+422
-342
No files matched your search
@@ -1,9 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { asc } from "drizzle-orm";
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { db, WebsiteHousekeepingPermissions } from "@/lib/db";
|
||||
import { redirectSafe } from "@/lib/foundation/security";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* Housekeeping table writes are retired. Runtime access uses ACL only.
|
||||
@@ -34,16 +35,19 @@ export async function bulkImportPermissions(
|
||||
export async function exportPermissions(): Promise<string> {
|
||||
await requirePermission(PERMS.SETTINGS_VIEW);
|
||||
|
||||
const perms = await prisma.websiteHousekeepingPermissions.findMany({
|
||||
orderBy: [{ groupName: "asc" }, { permission: "asc" }],
|
||||
select: {
|
||||
permission: true,
|
||||
minRank: true,
|
||||
description: true,
|
||||
groupName: true,
|
||||
dependsOn: true,
|
||||
},
|
||||
});
|
||||
const perms = await db
|
||||
.select({
|
||||
permission: WebsiteHousekeepingPermissions.permission,
|
||||
minRank: WebsiteHousekeepingPermissions.minRank,
|
||||
description: WebsiteHousekeepingPermissions.description,
|
||||
groupName: WebsiteHousekeepingPermissions.groupName,
|
||||
dependsOn: WebsiteHousekeepingPermissions.dependsOn,
|
||||
})
|
||||
.from(WebsiteHousekeepingPermissions)
|
||||
.orderBy(
|
||||
asc(WebsiteHousekeepingPermissions.groupName),
|
||||
asc(WebsiteHousekeepingPermissions.permission),
|
||||
);
|
||||
|
||||
return JSON.stringify(perms, null, 2);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use server";
|
||||
|
||||
import { requirePermission } from "@/lib/admin/guard";
|
||||
import { db, WebsiteBadges } from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function importBadgeFromRemote({
|
||||
code,
|
||||
@@ -15,21 +15,23 @@ export async function importBadgeFromRemote({
|
||||
}) {
|
||||
await requirePermission(PERMS.ASSETS_IMPORT);
|
||||
try {
|
||||
await prisma.websiteBadges.upsert({
|
||||
where: { badgeKey: code },
|
||||
update: {
|
||||
badgeName: name,
|
||||
badgeDescription: description,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
const now = new Date();
|
||||
await db
|
||||
.insert(WebsiteBadges)
|
||||
.values({
|
||||
badgeKey: code,
|
||||
badgeName: name,
|
||||
badgeDescription: description,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onDuplicateKeyUpdate({
|
||||
set: {
|
||||
badgeName: name,
|
||||
badgeDescription: description,
|
||||
updatedAt: now,
|
||||
},
|
||||
});
|
||||
return { ok: true as const };
|
||||
} catch {
|
||||
return { ok: false as const, error: "Failed to import badge" };
|
||||
|
||||
+13
-15
@@ -3,10 +3,10 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, RadioApplications } from "@/lib/db";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
// Column bounds from prisma/schema.prisma (radio_applications):
|
||||
// Column bounds from schema (radio_applications):
|
||||
// real_name VARCHAR(255); the rest are TEXT. age is an INT.
|
||||
const NAME_MAX = 255;
|
||||
const TEXT_MAX = 5000;
|
||||
@@ -66,19 +66,17 @@ export async function applyDj(formData: FormData): Promise<void> {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
const now = new Date();
|
||||
await prisma.radioApplications.create({
|
||||
data: {
|
||||
userId: BigInt(userId),
|
||||
realName,
|
||||
age,
|
||||
availability,
|
||||
motivation,
|
||||
experience: experience || null,
|
||||
musicStyle: musicStyle || null,
|
||||
status: "pending",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
await db.insert(RadioApplications).values({
|
||||
userId: BigInt(userId),
|
||||
realName,
|
||||
age,
|
||||
availability,
|
||||
motivation,
|
||||
experience: experience || null,
|
||||
musicStyle: musicStyle || null,
|
||||
status: "pending",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
outcome = "submitted";
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, RadioSongRequests } from "@/lib/db";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
const SONG_MAX = 255;
|
||||
@@ -52,15 +52,13 @@ export async function submitRequest(formData: FormData): Promise<void> {
|
||||
outcome = "empty";
|
||||
} else {
|
||||
const now = new Date();
|
||||
await prisma.radioSongRequests.create({
|
||||
data: {
|
||||
userId: BigInt(userId),
|
||||
songTitle: songTitle || null,
|
||||
artist: artist || null,
|
||||
submittedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
await db.insert(RadioSongRequests).values({
|
||||
userId: BigInt(userId),
|
||||
songTitle: songTitle || null,
|
||||
artist: artist || null,
|
||||
submittedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
outcome = "posted";
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, RadioShouts } from "@/lib/db";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { moderateOrThrow } from "@/lib/services/moderation";
|
||||
|
||||
@@ -77,13 +77,11 @@ export async function postShout(formData: FormData): Promise<void> {
|
||||
|
||||
if (!moderated) {
|
||||
const now = new Date();
|
||||
await prisma.radioShouts.create({
|
||||
data: {
|
||||
userId: BigInt(userId),
|
||||
message,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
await db.insert(RadioShouts).values({
|
||||
userId: BigInt(userId),
|
||||
message,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
outcome = "posted";
|
||||
}
|
||||
|
||||
+86
-65
@@ -1,12 +1,22 @@
|
||||
"use server";
|
||||
|
||||
import { desc, eq, inArray, sql } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
ClaimedReferralLogs,
|
||||
db,
|
||||
UserReferrals,
|
||||
WebsiteSetting,
|
||||
} from "@/lib/db";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
import { type CurrencyName, sendCurrency } from "@/lib/services/send-currency";
|
||||
import {
|
||||
type CurrencyName,
|
||||
currencyDb,
|
||||
sendCurrency,
|
||||
} from "@/lib/services/send-currency";
|
||||
|
||||
/**
|
||||
* Claim the referral reward for the SIGNED-IN user. Faithful to AtomCMS's
|
||||
@@ -60,51 +70,54 @@ export async function claimReferral(_formData: FormData): Promise<void> {
|
||||
} else {
|
||||
// Reward configuration (CMS-owned website_settings). AtomCMS defaults:
|
||||
// 5 referrals needed, 30 diamonds reward.
|
||||
const [neededRaw, amountRaw, currencyRaw] = await Promise.all([
|
||||
prisma.websiteSetting
|
||||
.findUnique({
|
||||
where: { key: "referrals_needed" },
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
prisma.websiteSetting
|
||||
.findUnique({
|
||||
where: { key: "referral_reward_amount" },
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
// The seeded key is referral_reward_currency_type; fall back to the
|
||||
// shorter referral_reward_currency name if that is what is configured.
|
||||
prisma.websiteSetting
|
||||
.findFirst({
|
||||
where: {
|
||||
key: {
|
||||
in: [
|
||||
"referral_reward_currency_type",
|
||||
"referral_reward_currency",
|
||||
],
|
||||
},
|
||||
},
|
||||
select: { value: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
]);
|
||||
const settingKeys = [
|
||||
"referrals_needed",
|
||||
"referral_reward_amount",
|
||||
"referral_reward_currency_type",
|
||||
"referral_reward_currency",
|
||||
] as const;
|
||||
|
||||
const needed = Number.parseInt(neededRaw?.value ?? "5", 10) || 5;
|
||||
const amount = Number.parseInt(amountRaw?.value ?? "30", 10);
|
||||
const currency = (currencyRaw?.value ?? "diamonds")
|
||||
let settings: Array<{ key: string; value: string }> = [];
|
||||
try {
|
||||
settings = await db
|
||||
.select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
|
||||
.from(WebsiteSetting)
|
||||
.where(inArray(WebsiteSetting.key, [...settingKeys]));
|
||||
} catch {
|
||||
settings = [];
|
||||
}
|
||||
|
||||
const byKey = new Map(settings.map((s) => [s.key, s.value]));
|
||||
const neededRaw = byKey.get("referrals_needed");
|
||||
const amountRaw = byKey.get("referral_reward_amount");
|
||||
// The seeded key is referral_reward_currency_type; fall back to the
|
||||
// shorter referral_reward_currency name if that is what is configured.
|
||||
const currencyRaw =
|
||||
byKey.get("referral_reward_currency_type") ??
|
||||
byKey.get("referral_reward_currency");
|
||||
|
||||
const needed = Number.parseInt(neededRaw ?? "5", 10) || 5;
|
||||
const amount = Number.parseInt(amountRaw ?? "30", 10);
|
||||
const currency = (currencyRaw ?? "diamonds")
|
||||
.trim()
|
||||
.toLowerCase() as CurrencyName;
|
||||
|
||||
// The user's referral tally lives in user_referrals (one row per user).
|
||||
const referrals = await prisma.userReferrals
|
||||
.findFirst({
|
||||
where: { userId },
|
||||
select: { id: true, referralsTotal: true },
|
||||
orderBy: { id: "desc" },
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
let referrals: { id: bigint; referralsTotal: bigint } | undefined;
|
||||
try {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: UserReferrals.id,
|
||||
referralsTotal: UserReferrals.referralsTotal,
|
||||
})
|
||||
.from(UserReferrals)
|
||||
.where(eq(UserReferrals.userId, userId))
|
||||
.orderBy(desc(UserReferrals.id))
|
||||
.limit(1);
|
||||
referrals = rows[0];
|
||||
} catch {
|
||||
referrals = undefined;
|
||||
}
|
||||
const total = referrals ? Number(referrals.referralsTotal) : 0;
|
||||
|
||||
if (!referrals || total <= 0) {
|
||||
@@ -117,40 +130,48 @@ export async function claimReferral(_formData: FormData): Promise<void> {
|
||||
} else {
|
||||
// Spend the threshold first so a concurrent double-submit can't claim
|
||||
// twice off the same balance, then deliver the reward and log it.
|
||||
await prisma.userReferrals.update({
|
||||
where: { id: referrals.id },
|
||||
data: { referralsTotal: { decrement: needed } },
|
||||
});
|
||||
await db
|
||||
.update(UserReferrals)
|
||||
.set({
|
||||
referralsTotal: sql`${UserReferrals.referralsTotal} - ${needed}`,
|
||||
})
|
||||
.where(eq(UserReferrals.id, referrals.id));
|
||||
|
||||
try {
|
||||
await sendCurrency({ rcon, db: prisma }, userId, currency, amount);
|
||||
await sendCurrency(
|
||||
{ rcon, db: currencyDb },
|
||||
userId,
|
||||
currency,
|
||||
amount,
|
||||
);
|
||||
} catch {
|
||||
// sendCurrency already falls back to a direct DB write; if it still
|
||||
// throws the spend stands. Roll the threshold back so the user isn't
|
||||
// charged for an undelivered reward.
|
||||
await prisma.userReferrals
|
||||
.update({
|
||||
where: { id: referrals.id },
|
||||
data: { referralsTotal: { increment: needed } },
|
||||
})
|
||||
.catch(() => {});
|
||||
try {
|
||||
await db
|
||||
.update(UserReferrals)
|
||||
.set({
|
||||
referralsTotal: sql`${UserReferrals.referralsTotal} + ${needed}`,
|
||||
})
|
||||
.where(eq(UserReferrals.id, referrals.id));
|
||||
} catch {
|
||||
/* best-effort rollback */
|
||||
}
|
||||
outcome = "error";
|
||||
throw new Error("currency-delivery-failed");
|
||||
}
|
||||
|
||||
await prisma.claimedReferralLogs
|
||||
.create({
|
||||
data: {
|
||||
userId,
|
||||
ipAddress: await clientIp(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.catch(() => {
|
||||
// Best-effort audit log; the reward already landed.
|
||||
try {
|
||||
await db.insert(ClaimedReferralLogs).values({
|
||||
userId,
|
||||
ipAddress: await clientIp(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
} catch {
|
||||
// Best-effort audit log; the reward already landed.
|
||||
}
|
||||
outcome = "claimed";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user