feat: Add subscription management system
- Add subscription fields to organizations (status, plan, trial_ends_at, etc.) - Add subscription middleware for access control - Add /auth/me returns subscription info - Add admin endpoints for subscription management: - GET/PUT /admin/organizations/:id/subscription - POST /admin/organizations/:id/pause - POST /admin/organizations/:id/activate - POST /admin/organizations/:id/extend-trial - GET /admin/subscriptions (with filters) - New orgs get 14-day trial automatically
This commit is contained in:
127
src/middleware/subscription.ts
Normal file
127
src/middleware/subscription.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Context, Next } from "@oak/oak";
|
||||
import { queryOne } from "../db/postgres.ts";
|
||||
import { AppError } from "./error.ts";
|
||||
|
||||
interface SubscriptionInfo {
|
||||
subscription_status: string;
|
||||
subscription_plan: string;
|
||||
trial_ends_at: Date | null;
|
||||
subscription_ends_at: Date | null;
|
||||
subscription_paused_at: Date | null;
|
||||
subscription_pause_reason: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware to check if organization has active subscription
|
||||
* Must be used AFTER authMiddleware
|
||||
*/
|
||||
export async function subscriptionMiddleware(ctx: Context, next: Next): Promise<void> {
|
||||
const user = ctx.state.auth?.user;
|
||||
|
||||
if (!user?.org_id) {
|
||||
throw new AppError("Nicht authentifiziert", 401);
|
||||
}
|
||||
|
||||
const org = await queryOne<SubscriptionInfo>(
|
||||
`SELECT subscription_status, subscription_plan, trial_ends_at,
|
||||
subscription_ends_at, subscription_paused_at, subscription_pause_reason
|
||||
FROM organizations WHERE id = $1`,
|
||||
[user.org_id]
|
||||
);
|
||||
|
||||
if (!org) {
|
||||
throw new AppError("Organisation nicht gefunden", 404);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
let isActive = false;
|
||||
let message = "";
|
||||
|
||||
switch (org.subscription_status) {
|
||||
case "active":
|
||||
// Check if subscription has ended
|
||||
if (org.subscription_ends_at && new Date(org.subscription_ends_at) < now) {
|
||||
message = "Ihr Abonnement ist abgelaufen. Bitte erneuern Sie Ihr Abo.";
|
||||
} else {
|
||||
isActive = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "trial":
|
||||
// Check if trial has ended
|
||||
if (org.trial_ends_at && new Date(org.trial_ends_at) < now) {
|
||||
message = "Ihre Testphase ist abgelaufen. Bitte wählen Sie ein Abo.";
|
||||
} else {
|
||||
isActive = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "paused":
|
||||
message = org.subscription_pause_reason || "Ihr Account wurde pausiert. Bitte kontaktieren Sie den Support.";
|
||||
break;
|
||||
|
||||
case "expired":
|
||||
message = "Ihr Abonnement ist abgelaufen. Bitte erneuern Sie Ihr Abo.";
|
||||
break;
|
||||
|
||||
case "cancelled":
|
||||
message = "Ihr Abonnement wurde gekündigt.";
|
||||
break;
|
||||
|
||||
default:
|
||||
message = "Unbekannter Abonnement-Status.";
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
throw new AppError(message, 402); // 402 Payment Required
|
||||
}
|
||||
|
||||
// Add subscription info to context for use in routes
|
||||
ctx.state.subscription = {
|
||||
status: org.subscription_status,
|
||||
plan: org.subscription_plan,
|
||||
trialEndsAt: org.trial_ends_at,
|
||||
subscriptionEndsAt: org.subscription_ends_at,
|
||||
};
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight check that just adds subscription info without blocking
|
||||
* Useful for endpoints that need subscription info but shouldn't be blocked
|
||||
*/
|
||||
export async function subscriptionInfoMiddleware(ctx: Context, next: Next): Promise<void> {
|
||||
const user = ctx.state.auth?.user;
|
||||
|
||||
if (user?.org_id) {
|
||||
const org = await queryOne<SubscriptionInfo>(
|
||||
`SELECT subscription_status, subscription_plan, trial_ends_at,
|
||||
subscription_ends_at, subscription_paused_at
|
||||
FROM organizations WHERE id = $1`,
|
||||
[user.org_id]
|
||||
);
|
||||
|
||||
if (org) {
|
||||
const now = new Date();
|
||||
let daysRemaining: number | null = null;
|
||||
|
||||
if (org.subscription_status === "trial" && org.trial_ends_at) {
|
||||
daysRemaining = Math.ceil((new Date(org.trial_ends_at).getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
} else if (org.subscription_status === "active" && org.subscription_ends_at) {
|
||||
daysRemaining = Math.ceil((new Date(org.subscription_ends_at).getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
ctx.state.subscription = {
|
||||
status: org.subscription_status,
|
||||
plan: org.subscription_plan,
|
||||
trialEndsAt: org.trial_ends_at,
|
||||
subscriptionEndsAt: org.subscription_ends_at,
|
||||
daysRemaining,
|
||||
isPaused: org.subscription_status === "paused",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
Reference in New Issue
Block a user