- plan-features.ts: globalBlocklist 'curated'|'full' (curated = 30-domain stub,
TODO real ~1-2k HaGeZi subset); maxAppDevices vs maxProtectedDevices split
(legend maxProtectedDevices: 2); mail 1/3/Infinity
- limit-enforcement structured errors on mail/connect, custom-domains/add, devices/enroll
({ error:'plan_limit', resource, current, limit }); approved-own-submissions already
excluded from custom-domain count (slot frees on approval)
- server/utils/downgrade-reconciliation.ts: founding-member exemption; re-upgrade
reactivates paused mail + degraded devices; downgrade pauses newest-N mail accounts
(isActive=false, pausedAt, pausedReason; pre-pause sets nextScanAt=now for a final
sweep — real direct IMAP scan is TODO/stub); degrades excess device profiles
(status='degraded', degradedAt); free → globalBlocklistGraceUntil = now+14d;
custom domains grandfathered
- set-plan.post.ts + stripe/webhook.post.ts: run reconciliation on plan change;
set-plan accepts { foundingMember } for testing
- GET /api/plan/change-preview?to=<plan>: gains/keeps/changes per resource (8 axes),
founding-member → direction 'same'
- me.get.ts: + foundingMember, globalBlocklistGraceUntil, planLimits block
- blocklist + mail-scan honour globalBlocklistGraceUntil (grace → treat as 'full')
- db: countMailConnections/getMailConnections exclude paused; getAllMailConnections;
getDeviceBlocklistMode (active|grace|passthrough|revoked)
- migration 20260511_tier_system_phase2 (profiles.founding_member +
global_blocklist_grace_until; mail_connections.paused_at/paused_reason;
protected_devices.degraded_at). prisma generate + build:backend clean.
TODOs (separate tickets): founding-member auto-counter on signup; real direct IMAP
final-scan (not just nextScanAt nudge); real curated blocklist data + wiring the
stub into the blocklist response for free users.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41 lines
1.4 KiB
TypeScript
41 lines
1.4 KiB
TypeScript
import { getProfile } from "../../db/profile";
|
|
import { getPlanLimits } from "../../utils/plan-features";
|
|
|
|
export default defineEventHandler(async (event) => {
|
|
const user = await requireUser(event);
|
|
const dbProfile = await getProfile(user.id);
|
|
|
|
const plan = (dbProfile?.plan === "premium"
|
|
? "legend"
|
|
: dbProfile?.plan === "standard"
|
|
? "pro"
|
|
: dbProfile?.plan ?? "free") as "free" | "pro" | "legend";
|
|
|
|
const limits = getPlanLimits(plan);
|
|
|
|
return {
|
|
id: user.id,
|
|
email: user.email,
|
|
username: dbProfile?.username ?? "",
|
|
nickname: dbProfile?.nickname ?? null,
|
|
avatar: dbProfile?.avatar ?? null,
|
|
plan,
|
|
foundingMember: dbProfile?.foundingMember ?? false,
|
|
streak: dbProfile?.streak ?? 0,
|
|
created_at: dbProfile?.createdAt?.toISOString() ?? user.created_at,
|
|
// Für useUserPlan im Frontend — Key-Subset der PlanLimits
|
|
planLimits: {
|
|
customDomains: limits.customDomains,
|
|
domainRefill: limits.domainRefill,
|
|
mailAgents: limits.mailAgents === Infinity ? null : limits.mailAgents,
|
|
globalBlocklist: limits.globalBlocklist,
|
|
maxAppDevices: limits.maxAppDevices,
|
|
maxProtectedDevices: limits.maxProtectedDevices,
|
|
canCreateGroup: limits.canCreateGroup,
|
|
canAddToBlocklist: limits.canAddToBlocklist,
|
|
},
|
|
globalBlocklistGraceUntil:
|
|
dbProfile?.globalBlocklistGraceUntil?.toISOString() ?? null,
|
|
};
|
|
});
|