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 securityPlugin from './plugins/security.js';
|
||||||
import rateLimitPlugin from './plugins/rateLimit.js';
|
import rateLimitPlugin from './plugins/rateLimit.js';
|
||||||
import authPlugin from './plugins/auth.js';
|
import authPlugin from './plugins/auth.js';
|
||||||
|
import subscriptionPlugin from './plugins/subscription.js';
|
||||||
import { authRoutes } from './routes/auth.js';
|
import { authRoutes } from './routes/auth.js';
|
||||||
import { env } from './config/env.js';
|
import { env } from './config/env.js';
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
@@ -72,6 +73,7 @@ export async function buildApp(): Promise<FastifyInstance> {
|
|||||||
await app.register(securityPlugin);
|
await app.register(securityPlugin);
|
||||||
await app.register(rateLimitPlugin);
|
await app.register(rateLimitPlugin);
|
||||||
await app.register(authPlugin);
|
await app.register(authPlugin);
|
||||||
|
await app.register(subscriptionPlugin);
|
||||||
await app.register(authRoutes, { prefix: '/auth' });
|
await app.register(authRoutes, { prefix: '/auth' });
|
||||||
|
|
||||||
app.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
|
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