PRO cutoff: an account that signed up before 2026-12-01 keeps the whole studio with its Activated Pro box ticked by the calendar, signups from the cutoff on start basic
This commit is contained in:
@@ -86,7 +86,7 @@ const ADMIN_EMAILS = new Set(
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const isAdmin = (user: User) => ADMIN_EMAILS.has(user.email.toLowerCase());
|
||||
const isAdmin = (user: Pick<User, 'email'>) => ADMIN_EMAILS.has(user.email.toLowerCase());
|
||||
|
||||
// What an account is worth. Being signed in is the basic tier and nothing more
|
||||
// is required of it (see requireMember): the account has its own recipes and
|
||||
@@ -94,13 +94,21 @@ const isAdmin = (user: User) => ADMIN_EMAILS.has(user.email.toLowerCase());
|
||||
// carries on top. Admins come from the deployment's own allowlist — trusted by
|
||||
// construction, so no letter is needed and a broken relay cannot lock the
|
||||
// operator out of their own site.
|
||||
const isVerified = (user: User) => user.emailVerified === 1 || isAdmin(user);
|
||||
const isVerified = (user: Pick<User, 'email' | 'emailVerified'>) => user.emailVerified === 1 || isAdmin(user);
|
||||
|
||||
// The PRO tier itself: a proven address, an admin, or a grant the operator
|
||||
// ticked in the users table. The grant only ever adds — unticking an account
|
||||
// that has already proven its address leaves it PRO, because the address is
|
||||
// still the stronger proof of the two.
|
||||
const isPro = (user: User) => isVerified(user) || user.pro === 1;
|
||||
// The one date in the tier model: an account that signed up before it keeps the
|
||||
// studio it was promised, so its "Activated Pro" box is ticked by the calendar
|
||||
// rather than by the operator. Signups from the cutoff on start basic and the
|
||||
// box is theirs to tick (or the address to prove).
|
||||
const PRO_CUTOFF_MS = Date.parse('2026-12-01T00:00:00Z');
|
||||
const grandfathered = (createdAt: string) => Date.parse(createdAt) < PRO_CUTOFF_MS;
|
||||
|
||||
// The PRO tier itself: a proven address, an admin, a grandfathered signup, or a
|
||||
// grant the operator ticked in the users table. The grant only ever adds —
|
||||
// unticking an account that has already proven its address leaves it PRO,
|
||||
// because the address is still the stronger proof of the two.
|
||||
const isPro = (user: Pick<User, 'email' | 'emailVerified' | 'pro' | 'createdAt'>) =>
|
||||
isVerified(user) || user.pro === 1 || grandfathered(user.createdAt);
|
||||
|
||||
// The public shape of an account. `admin` is the allowlist's answer, so the
|
||||
// client can decide whether to offer /admin without a second round trip — and
|
||||
@@ -1011,9 +1019,10 @@ app.get('/api/admin/users', async (req, reply) => {
|
||||
admin: ADMIN_EMAILS.has(u.email),
|
||||
blocked: !!u.blocked,
|
||||
removed: !!u.deletedAt,
|
||||
// An allowlisted account is PRO by construction; its stored box is not
|
||||
// what decides, so the table shows the truth of `isPro`.
|
||||
pro: !!u.pro || ADMIN_EMAILS.has(u.email.toLowerCase()),
|
||||
// The box shows the truth of `isPro`, not the stored column: an
|
||||
// allowlisted operator and a signup from before the cutoff are PRO
|
||||
// whatever the column says, and unticking them cannot take that away.
|
||||
pro: isPro(u),
|
||||
})),
|
||||
});
|
||||
});
|
||||
@@ -1062,7 +1071,7 @@ app.patch<{ Params: { id: string } }>('/api/admin/users/:id', async (req, reply)
|
||||
const row = listUsersWithCounts().find((u) => u.id === id);
|
||||
return reply
|
||||
.status(200)
|
||||
.send({ user: { ...row, blocked: !!row?.blocked, removed: !!row?.deletedAt, pro: !!row?.pro } });
|
||||
.send({ user: { ...row, blocked: !!row?.blocked, removed: !!row?.deletedAt, pro: !!row && isPro(row) } });
|
||||
});
|
||||
|
||||
app.delete<{ Params: { id: string } }>('/api/admin/users/:id', async (req, reply) => {
|
||||
|
||||
Reference in New Issue
Block a user