feat: add subscription middleware
Made-with: Cursor
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "samreshu_docs"]
|
||||
path = samreshu_docs
|
||||
url = https://git.vakanaut.ru/admin/samreshu_docs.git
|
||||
1
samreshu_docs
Submodule
1
samreshu_docs
Submodule
Submodule samreshu_docs added at 99cd8ae727
@@ -5,6 +5,7 @@ import redisPlugin from './plugins/redis.js';
|
||||
import securityPlugin from './plugins/security.js';
|
||||
import rateLimitPlugin from './plugins/rateLimit.js';
|
||||
import authPlugin from './plugins/auth.js';
|
||||
import subscriptionPlugin from './plugins/subscription.js';
|
||||
import { authRoutes } from './routes/auth.js';
|
||||
import { env } from './config/env.js';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
@@ -72,6 +73,7 @@ export async function buildApp(): Promise<FastifyInstance> {
|
||||
await app.register(securityPlugin);
|
||||
await app.register(rateLimitPlugin);
|
||||
await app.register(authPlugin);
|
||||
await app.register(subscriptionPlugin);
|
||||
await app.register(authRoutes, { prefix: '/auth' });
|
||||
|
||||
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
|
||||
|
||||
71
src/plugins/subscription.ts
Normal file
71
src/plugins/subscription.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
||||
import fp from 'fastify-plugin';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { subscriptions } from '../db/schema/subscriptions.js';
|
||||
import { forbidden } from '../utils/errors.js';
|
||||
|
||||
export type SubscriptionInfo = {
|
||||
plan: 'free' | 'pro';
|
||||
status: 'active' | 'trialing' | 'cancelled' | 'expired';
|
||||
isPro: boolean;
|
||||
expiresAt: Date | null;
|
||||
};
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
subscription?: SubscriptionInfo | null;
|
||||
}
|
||||
interface FastifyInstance {
|
||||
withSubscription: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
||||
requirePro: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubscription(db: FastifyInstance['db'], userId: string): Promise<SubscriptionInfo | null> {
|
||||
const [sub] = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.where(eq(subscriptions.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
if (!sub) return null;
|
||||
|
||||
const now = new Date();
|
||||
const isExpired = sub.expiresAt && sub.expiresAt < now;
|
||||
const isPro =
|
||||
sub.plan === 'pro' &&
|
||||
(sub.status === 'active' || sub.status === 'trialing') &&
|
||||
!isExpired;
|
||||
|
||||
return {
|
||||
plan: sub.plan as 'free' | 'pro',
|
||||
status: sub.status as SubscriptionInfo['status'],
|
||||
isPro,
|
||||
expiresAt: sub.expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function requirePro(req: FastifyRequest, _reply: FastifyReply): Promise<void> {
|
||||
const sub = req.subscription;
|
||||
|
||||
if (!sub?.isPro) {
|
||||
throw forbidden('Pro subscription required');
|
||||
}
|
||||
}
|
||||
|
||||
const subscriptionPlugin = async (app: FastifyInstance) => {
|
||||
app.decorateRequest('subscription', undefined);
|
||||
|
||||
app.decorate('withSubscription', async (req: FastifyRequest, _reply: FastifyReply) => {
|
||||
if (!req.user?.id) return;
|
||||
const sub = await loadSubscription(app.db, req.user.id);
|
||||
req.subscription = sub;
|
||||
});
|
||||
|
||||
app.decorate('requirePro', requirePro);
|
||||
};
|
||||
|
||||
export default fp(subscriptionPlugin, {
|
||||
name: 'subscription',
|
||||
dependencies: ['database', 'auth'],
|
||||
});
|
||||
Reference in New Issue
Block a user