feat(auth): include subscription tier in token and session payload
- Adds subscription tier info to access/refresh tokens. - Stores tier details in session Redis record. - Returns tier data in login/register response. - Uses default tier when user has none.
This commit is contained in:
1 parent
7f42def478
commit
d27526e026
20 files changed
+2133
-40
No files matched your search
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* Queries para gerenciamento de subscription tiers
|
||||
*/
|
||||
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "../app";
|
||||
|
||||
export interface SubscriptionTierSummary {
|
||||
subscription_tier_id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
level: number;
|
||||
price_monthly: Prisma.Decimal | null;
|
||||
price_yearly: Prisma.Decimal | null;
|
||||
is_active: boolean;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
features?: Array<{
|
||||
feature_id: number;
|
||||
feature_key: string;
|
||||
label: string;
|
||||
description: string | null;
|
||||
sort_order: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CreateSubscriptionTierInput {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string | null;
|
||||
level?: number;
|
||||
price_monthly?: number | null;
|
||||
price_yearly?: number | null;
|
||||
is_active?: boolean;
|
||||
features?: Array<{
|
||||
feature_key: string;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
sort_order?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionTierInput {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
description?: string | null;
|
||||
level?: number;
|
||||
price_monthly?: number | null;
|
||||
price_yearly?: number | null;
|
||||
is_active?: boolean;
|
||||
features?: Array<{
|
||||
feature_id?: number;
|
||||
feature_key: string;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
sort_order?: number;
|
||||
_delete?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
const tierSelect = {
|
||||
subscription_tier_id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
level: true,
|
||||
price_monthly: true,
|
||||
price_yearly: true,
|
||||
is_active: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
};
|
||||
|
||||
const featureSelect = {
|
||||
feature_id: true,
|
||||
feature_key: true,
|
||||
label: true,
|
||||
description: true,
|
||||
sort_order: true,
|
||||
};
|
||||
|
||||
export async function getAllSubscriptionTiers(
|
||||
includeFeatures = false
|
||||
): Promise<SubscriptionTierSummary[]> {
|
||||
const tiers = await prisma.subscriptionTier.findMany({
|
||||
select: {
|
||||
...tierSelect,
|
||||
features: includeFeatures
|
||||
? {
|
||||
select: featureSelect,
|
||||
orderBy: { sort_order: "asc" },
|
||||
}
|
||||
: false,
|
||||
},
|
||||
orderBy: [
|
||||
{ level: "asc" },
|
||||
{ subscription_tier_id: "asc" },
|
||||
],
|
||||
});
|
||||
|
||||
return tiers as SubscriptionTierSummary[];
|
||||
}
|
||||
|
||||
export async function getSubscriptionTierBySlug(
|
||||
slug: string,
|
||||
includeFeatures = false
|
||||
): Promise<SubscriptionTierSummary | null> {
|
||||
const tier = await prisma.subscriptionTier.findUnique({
|
||||
where: { slug },
|
||||
select: {
|
||||
...tierSelect,
|
||||
features: includeFeatures
|
||||
? {
|
||||
select: featureSelect,
|
||||
orderBy: { sort_order: "asc" },
|
||||
}
|
||||
: false,
|
||||
},
|
||||
});
|
||||
|
||||
return tier as SubscriptionTierSummary | null;
|
||||
}
|
||||
|
||||
export async function getSubscriptionTierById(
|
||||
tierId: number,
|
||||
includeFeatures = false
|
||||
): Promise<SubscriptionTierSummary | null> {
|
||||
const tier = await prisma.subscriptionTier.findUnique({
|
||||
where: { subscription_tier_id: tierId },
|
||||
select: {
|
||||
...tierSelect,
|
||||
features: includeFeatures
|
||||
? {
|
||||
select: featureSelect,
|
||||
orderBy: { sort_order: "asc" },
|
||||
}
|
||||
: false,
|
||||
},
|
||||
});
|
||||
|
||||
return tier as SubscriptionTierSummary | null;
|
||||
}
|
||||
|
||||
export async function getDefaultSubscriptionTier(): Promise<SubscriptionTierSummary | null> {
|
||||
const tier = await prisma.subscriptionTier.findFirst({
|
||||
where: { slug: "free" },
|
||||
select: tierSelect,
|
||||
});
|
||||
|
||||
return tier as SubscriptionTierSummary | null;
|
||||
}
|
||||
|
||||
export async function createSubscriptionTier(
|
||||
input: CreateSubscriptionTierInput
|
||||
): Promise<SubscriptionTierSummary> {
|
||||
const tier = await prisma.subscriptionTier.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
slug: input.slug,
|
||||
description: input.description ?? null,
|
||||
level: input.level ?? 0,
|
||||
price_monthly:
|
||||
input.price_monthly !== undefined && input.price_monthly !== null
|
||||
? new Prisma.Decimal(input.price_monthly)
|
||||
: null,
|
||||
price_yearly:
|
||||
input.price_yearly !== undefined && input.price_yearly !== null
|
||||
? new Prisma.Decimal(input.price_yearly)
|
||||
: null,
|
||||
is_active: input.is_active ?? true,
|
||||
features: input.features
|
||||
? {
|
||||
create: input.features.map((feature, index) => ({
|
||||
feature_key: feature.feature_key,
|
||||
label: feature.label,
|
||||
description: feature.description ?? null,
|
||||
sort_order: feature.sort_order ?? index,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
select: {
|
||||
...tierSelect,
|
||||
features: {
|
||||
select: featureSelect,
|
||||
orderBy: { sort_order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return tier as SubscriptionTierSummary;
|
||||
}
|
||||
|
||||
export async function updateSubscriptionTier(
|
||||
tierId: number,
|
||||
input: UpdateSubscriptionTierInput
|
||||
): Promise<SubscriptionTierSummary> {
|
||||
const tier = await prisma.subscriptionTier.update({
|
||||
where: { subscription_tier_id: tierId },
|
||||
data: {
|
||||
name: input.name ?? undefined,
|
||||
slug: input.slug ?? undefined,
|
||||
description:
|
||||
input.description !== undefined ? input.description : undefined,
|
||||
level: input.level ?? undefined,
|
||||
price_monthly:
|
||||
input.price_monthly !== undefined
|
||||
? input.price_monthly === null
|
||||
? null
|
||||
: new Prisma.Decimal(input.price_monthly)
|
||||
: undefined,
|
||||
price_yearly:
|
||||
input.price_yearly !== undefined
|
||||
? input.price_yearly === null
|
||||
? null
|
||||
: new Prisma.Decimal(input.price_yearly)
|
||||
: undefined,
|
||||
is_active: input.is_active ?? undefined,
|
||||
},
|
||||
select: {
|
||||
...tierSelect,
|
||||
features: {
|
||||
select: featureSelect,
|
||||
orderBy: { sort_order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (input.features) {
|
||||
await prisma.subscriptionTierFeature.deleteMany({
|
||||
where: { subscription_tier_id: tierId },
|
||||
});
|
||||
|
||||
if (input.features.length > 0) {
|
||||
await prisma.subscriptionTierFeature.createMany({
|
||||
data: input.features
|
||||
.filter((feature) => !feature._delete)
|
||||
.map((feature, index) => ({
|
||||
subscription_tier_id: tierId,
|
||||
feature_key: feature.feature_key,
|
||||
label: feature.label,
|
||||
description: feature.description ?? null,
|
||||
sort_order: feature.sort_order ?? index,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedTier = await prisma.subscriptionTier.findUnique({
|
||||
where: { subscription_tier_id: tierId },
|
||||
select: {
|
||||
...tierSelect,
|
||||
features: {
|
||||
select: featureSelect,
|
||||
orderBy: { sort_order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return updatedTier as SubscriptionTierSummary;
|
||||
}
|
||||
|
||||
export async function deleteSubscriptionTier(tierId: number): Promise<void> {
|
||||
await prisma.subscriptionTier.delete({
|
||||
where: { subscription_tier_id: tierId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function assignUserToSubscriptionTier(
|
||||
userId: number,
|
||||
tierId: number | null
|
||||
): Promise<void> {
|
||||
await prisma.user.update({
|
||||
where: { user_id: userId },
|
||||
data:
|
||||
tierId === null
|
||||
? {
|
||||
subscription_tier: {
|
||||
disconnect: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
subscription_tier: {
|
||||
connect: {
|
||||
subscription_tier_id: tierId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function countUsersInSubscriptionTier(tierId: number): Promise<number> {
|
||||
return prisma.user.count({
|
||||
where: { subscription_tier_id: tierId },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { prisma } from "../app";
|
||||
import { getDefaultSubscriptionTier } from "./subscription-tiers";
|
||||
import type { User } from "@prisma/client";
|
||||
|
||||
export interface CreateUserData {
|
||||
@@ -21,6 +22,7 @@ export interface UpdateUserData {
|
||||
last_name?: string;
|
||||
phone?: string;
|
||||
whatsapp_notifications?: boolean;
|
||||
subscription_tier_id?: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,7 @@ export interface UpdateUserData {
|
||||
export async function createUser(
|
||||
data: CreateUserData
|
||||
): Promise<{ userId: number; username: string; email: string }> {
|
||||
const defaultTier = await getDefaultSubscriptionTier();
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
@@ -38,6 +41,7 @@ export async function createUser(
|
||||
last_name: data.last_name,
|
||||
phone: data.phone || null,
|
||||
cpf: data.cpf || null,
|
||||
subscription_tier_id: defaultTier?.subscription_tier_id ?? undefined,
|
||||
},
|
||||
select: {
|
||||
user_id: true,
|
||||
@@ -56,10 +60,29 @@ export async function createUser(
|
||||
/**
|
||||
* Busca usuário por email (inclui password_hash para login)
|
||||
*/
|
||||
export async function getUserByEmail(email: string): Promise<User | null> {
|
||||
return await prisma.user.findUnique({
|
||||
type UserWithTier = User & {
|
||||
subscription_tier?: {
|
||||
subscription_tier_id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
level: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function getUserByEmail(email: string): Promise<UserWithTier | null> {
|
||||
return (await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
include: {
|
||||
subscription_tier: {
|
||||
select: {
|
||||
subscription_tier_id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as UserWithTier | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,10 +90,20 @@ export async function getUserByEmail(email: string): Promise<User | null> {
|
||||
*/
|
||||
export async function getUserByUsername(
|
||||
username: string
|
||||
): Promise<User | null> {
|
||||
return await prisma.user.findUnique({
|
||||
): Promise<UserWithTier | null> {
|
||||
return (await prisma.user.findUnique({
|
||||
where: { username },
|
||||
});
|
||||
include: {
|
||||
subscription_tier: {
|
||||
select: {
|
||||
subscription_tier_id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})) as UserWithTier | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +125,12 @@ type PublicUser = {
|
||||
last_login: Date | null;
|
||||
is_active: boolean;
|
||||
is_verified: boolean;
|
||||
subscription_tier?: {
|
||||
subscription_tier_id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
level: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
export async function getUserById(userId: number): Promise<PublicUser | null> {
|
||||
@@ -113,6 +152,14 @@ export async function getUserById(userId: number): Promise<PublicUser | null> {
|
||||
last_login: true,
|
||||
is_active: true,
|
||||
is_verified: true,
|
||||
subscription_tier: {
|
||||
select: {
|
||||
subscription_tier_id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as PublicUser | null;
|
||||
}
|
||||
@@ -144,6 +191,15 @@ export async function updateUser(
|
||||
if (data.phone !== undefined) updateData.phone = data.phone || null;
|
||||
if (data.whatsapp_notifications !== undefined)
|
||||
updateData.whatsapp_notifications = data.whatsapp_notifications;
|
||||
if (data.subscription_tier_id !== undefined) {
|
||||
if (data.subscription_tier_id === null) {
|
||||
updateData.subscription_tier = { disconnect: true };
|
||||
} else {
|
||||
updateData.subscription_tier = {
|
||||
connect: { subscription_tier_id: data.subscription_tier_id },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const user = await prisma.user.update({
|
||||
where: { user_id: userId },
|
||||
@@ -164,6 +220,14 @@ export async function updateUser(
|
||||
last_login: true,
|
||||
is_active: true,
|
||||
is_verified: true,
|
||||
subscription_tier: {
|
||||
select: {
|
||||
subscription_tier_id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user