feat: integrate new modules (#5087)
This commit is contained in:
@@ -8,6 +8,20 @@ async function main() {
|
|||||||
data: {
|
data: {
|
||||||
...userA,
|
...userA,
|
||||||
password: await hash(userA.password),
|
password: await hash(userA.password),
|
||||||
|
features: {
|
||||||
|
create: {
|
||||||
|
reason: 'created by api sign up',
|
||||||
|
activated: true,
|
||||||
|
feature: {
|
||||||
|
connect: {
|
||||||
|
feature_version: {
|
||||||
|
feature: 'free_plan_v1',
|
||||||
|
version: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,30 @@ import {
|
|||||||
FeatureKind,
|
FeatureKind,
|
||||||
Features,
|
Features,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
} from '../../modules/features/types';
|
} from '../../modules/features';
|
||||||
import { Quotas } from '../../modules/quota/types';
|
import { Quotas } from '../../modules/quota/types';
|
||||||
import { PrismaService } from '../../prisma';
|
import { PrismaService } from '../../prisma';
|
||||||
|
|
||||||
|
export class UserFeaturesInit1698652531198 {
|
||||||
|
// do the migration
|
||||||
|
static async up(db: PrismaService) {
|
||||||
|
// upgrade features from lower version to higher version
|
||||||
|
for (const feature of Features) {
|
||||||
|
await upsertFeature(db, feature);
|
||||||
|
}
|
||||||
|
await migrateNewFeatureTable(db);
|
||||||
|
|
||||||
|
for (const quota of Quotas) {
|
||||||
|
await upsertFeature(db, quota);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// revert the migration
|
||||||
|
static async down(_db: PrismaService) {
|
||||||
|
// TODO: revert the migration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// upgrade features from lower version to higher version
|
// upgrade features from lower version to higher version
|
||||||
async function upsertFeature(
|
async function upsertFeature(
|
||||||
db: PrismaService,
|
db: PrismaService,
|
||||||
@@ -34,26 +54,6 @@ async function upsertFeature(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserFeaturesInit1698652531198 {
|
|
||||||
// do the migration
|
|
||||||
static async up(db: PrismaService) {
|
|
||||||
// upgrade features from lower version to higher version
|
|
||||||
for (const feature of Features) {
|
|
||||||
await upsertFeature(db, feature);
|
|
||||||
}
|
|
||||||
await migrateNewFeatureTable(db);
|
|
||||||
|
|
||||||
for (const quota of Quotas) {
|
|
||||||
await upsertFeature(db, quota);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// revert the migration
|
|
||||||
static async down(_db: PrismaService) {
|
|
||||||
// TODO: revert the migration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateNewFeatureTable(prisma: PrismaService) {
|
async function migrateNewFeatureTable(prisma: PrismaService) {
|
||||||
const waitingList = await prisma.newFeaturesWaitingList.findMany();
|
const waitingList = await prisma.newFeaturesWaitingList.findMany();
|
||||||
for (const oldUser of waitingList) {
|
for (const oldUser of waitingList) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { nanoid } from 'nanoid';
|
|||||||
import { Config } from '../../config';
|
import { Config } from '../../config';
|
||||||
import { SessionService } from '../../session';
|
import { SessionService } from '../../session';
|
||||||
import { CloudThrottlerGuard, Throttle } from '../../throttler';
|
import { CloudThrottlerGuard, Throttle } from '../../throttler';
|
||||||
import { UserType } from '../users/resolver';
|
import { UserType } from '../users';
|
||||||
import { Auth, CurrentUser } from './guard';
|
import { Auth, CurrentUser } from './guard';
|
||||||
import { AuthService } from './service';
|
import { AuthService } from './service';
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Config } from '../../config';
|
|||||||
import { type EventPayload, OnEvent } from '../../event';
|
import { type EventPayload, OnEvent } from '../../event';
|
||||||
import { metrics } from '../../metrics';
|
import { metrics } from '../../metrics';
|
||||||
import { PrismaService } from '../../prisma';
|
import { PrismaService } from '../../prisma';
|
||||||
import { SubscriptionStatus } from '../payment/service';
|
import { QuotaService } from '../quota';
|
||||||
import { Permission } from '../workspaces/types';
|
import { Permission } from '../workspaces/types';
|
||||||
import { isEmptyBuffer } from './manager';
|
import { isEmptyBuffer } from './manager';
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ export class DocHistoryManager {
|
|||||||
private readonly logger = new Logger(DocHistoryManager.name);
|
private readonly logger = new Logger(DocHistoryManager.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly db: PrismaService
|
private readonly db: PrismaService,
|
||||||
|
private readonly quota: QuotaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnEvent('workspace.deleted')
|
@OnEvent('workspace.deleted')
|
||||||
@@ -222,9 +223,6 @@ export class DocHistoryManager {
|
|||||||
return history.timestamp;
|
return history.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @todo(@darkskygit) refactor with [Usage Control] system
|
|
||||||
*/
|
|
||||||
async getExpiredDateFromNow(workspaceId: string) {
|
async getExpiredDateFromNow(workspaceId: string) {
|
||||||
const permission = await this.db.workspaceUserPermission.findFirst({
|
const permission = await this.db.workspaceUserPermission.findFirst({
|
||||||
select: {
|
select: {
|
||||||
@@ -241,25 +239,9 @@ export class DocHistoryManager {
|
|||||||
throw new Error('Workspace owner not found');
|
throw new Error('Workspace owner not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const sub = await this.db.userSubscription.findFirst({
|
const quota = await this.quota.getUserQuota(permission.userId);
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
userId: permission.userId,
|
|
||||||
status: SubscriptionStatus.Active,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Date(
|
return new Date(Date.now() + quota.feature.configs.historyPeriod);
|
||||||
Date.now() +
|
|
||||||
1000 *
|
|
||||||
60 *
|
|
||||||
60 *
|
|
||||||
24 *
|
|
||||||
// 30 days for subscription user, 7 days for free user
|
|
||||||
(sub ? 30 : 7)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT /* everyday at 12am */)
|
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT /* everyday at 12am */)
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { QuotaModule } from '../quota';
|
||||||
import { DocHistoryManager } from './history';
|
import { DocHistoryManager } from './history';
|
||||||
import { DocManager } from './manager';
|
import { DocManager } from './manager';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [QuotaModule],
|
||||||
providers: [DocManager, DocHistoryManager],
|
providers: [DocManager, DocHistoryManager],
|
||||||
exports: [DocManager, DocHistoryManager],
|
exports: [DocManager, DocHistoryManager],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { PrismaService } from '../../prisma';
|
|||||||
import { FeatureService } from './configure';
|
import { FeatureService } from './configure';
|
||||||
import { FeatureType } from './types';
|
import { FeatureType } from './types';
|
||||||
|
|
||||||
export enum NewFeaturesKind {
|
enum NewFeaturesKind {
|
||||||
EarlyAccess,
|
EarlyAccess,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { FeatureModule } from '../features';
|
import { FeatureModule } from '../features';
|
||||||
|
import { QuotaModule } from '../quota';
|
||||||
import { SubscriptionResolver, UserSubscriptionResolver } from './resolver';
|
import { SubscriptionResolver, UserSubscriptionResolver } from './resolver';
|
||||||
import { ScheduleManager } from './schedule';
|
import { ScheduleManager } from './schedule';
|
||||||
import { SubscriptionService } from './service';
|
import { SubscriptionService } from './service';
|
||||||
@@ -8,7 +9,7 @@ import { StripeProvider } from './stripe';
|
|||||||
import { StripeWebhook } from './webhook';
|
import { StripeWebhook } from './webhook';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [FeatureModule],
|
imports: [FeatureModule, QuotaModule],
|
||||||
providers: [
|
providers: [
|
||||||
ScheduleManager,
|
ScheduleManager,
|
||||||
StripeProvider,
|
StripeProvider,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Stripe from 'stripe';
|
|||||||
import { Config } from '../../config';
|
import { Config } from '../../config';
|
||||||
import { PrismaService } from '../../prisma';
|
import { PrismaService } from '../../prisma';
|
||||||
import { FeatureManagementService } from '../features';
|
import { FeatureManagementService } from '../features';
|
||||||
|
import { QuotaService, QuotaType } from '../quota';
|
||||||
import { ScheduleManager } from './schedule';
|
import { ScheduleManager } from './schedule';
|
||||||
|
|
||||||
const OnEvent = (
|
const OnEvent = (
|
||||||
@@ -60,6 +61,11 @@ export enum SubscriptionStatus {
|
|||||||
Trialing = 'trialing',
|
Trialing = 'trialing',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SubscriptionActivated: Stripe.Subscription.Status[] = [
|
||||||
|
SubscriptionStatus.Active,
|
||||||
|
SubscriptionStatus.Trialing,
|
||||||
|
];
|
||||||
|
|
||||||
export enum InvoiceStatus {
|
export enum InvoiceStatus {
|
||||||
Draft = 'draft',
|
Draft = 'draft',
|
||||||
Open = 'open',
|
Open = 'open',
|
||||||
@@ -83,7 +89,8 @@ export class SubscriptionService {
|
|||||||
private readonly stripe: Stripe,
|
private readonly stripe: Stripe,
|
||||||
private readonly db: PrismaService,
|
private readonly db: PrismaService,
|
||||||
private readonly scheduleManager: ScheduleManager,
|
private readonly scheduleManager: ScheduleManager,
|
||||||
private readonly features: FeatureManagementService
|
private readonly features: FeatureManagementService,
|
||||||
|
private readonly quota: QuotaService
|
||||||
) {
|
) {
|
||||||
this.paymentConfig = config.payment;
|
this.paymentConfig = config.payment;
|
||||||
|
|
||||||
@@ -471,6 +478,16 @@ export class SubscriptionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getPlanQuota(plan: SubscriptionPlan) {
|
||||||
|
if (plan === SubscriptionPlan.Free) {
|
||||||
|
return QuotaType.Quota_FreePlanV1;
|
||||||
|
} else if (plan === SubscriptionPlan.Pro) {
|
||||||
|
return QuotaType.Quota_ProPlanV1;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown plan: ${plan}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async saveSubscription(
|
private async saveSubscription(
|
||||||
user: User,
|
user: User,
|
||||||
subscription: Stripe.Subscription,
|
subscription: Stripe.Subscription,
|
||||||
@@ -483,23 +500,28 @@ export class SubscriptionService {
|
|||||||
subscription = await this.stripe.subscriptions.retrieve(subscription.id);
|
subscription = await this.stripe.subscriptions.retrieve(subscription.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get next bill date from upcoming invoice
|
|
||||||
// see https://stripe.com/docs/api/invoices/upcoming
|
|
||||||
let nextBillAt: Date | null = null;
|
|
||||||
if (
|
|
||||||
(subscription.status === SubscriptionStatus.Active ||
|
|
||||||
subscription.status === SubscriptionStatus.Trialing) &&
|
|
||||||
!subscription.canceled_at
|
|
||||||
) {
|
|
||||||
nextBillAt = new Date(subscription.current_period_end * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
const price = subscription.items.data[0].price;
|
const price = subscription.items.data[0].price;
|
||||||
if (!price.lookup_key) {
|
if (!price.lookup_key) {
|
||||||
throw new Error('Unexpected subscription with no key');
|
throw new Error('Unexpected subscription with no key');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [plan, recurring] = decodeLookupKey(price.lookup_key);
|
const [plan, recurring] = decodeLookupKey(price.lookup_key);
|
||||||
|
const planActivated = SubscriptionActivated.includes(subscription.status);
|
||||||
|
|
||||||
|
let nextBillAt: Date | null = null;
|
||||||
|
if (planActivated) {
|
||||||
|
// update user's quota if plan activated
|
||||||
|
await this.quota.switchUserQuota(user.id, this.getPlanQuota(plan));
|
||||||
|
|
||||||
|
// get next bill date from upcoming invoice
|
||||||
|
// see https://stripe.com/docs/api/invoices/upcoming
|
||||||
|
if (!subscription.canceled_at) {
|
||||||
|
nextBillAt = new Date(subscription.current_period_end * 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// switch to free plan if subscription is canceled
|
||||||
|
await this.quota.switchUserQuota(user.id, QuotaType.Quota_FreePlanV1);
|
||||||
|
}
|
||||||
|
|
||||||
const commonData = {
|
const commonData = {
|
||||||
start: new Date(subscription.current_period_start * 1000),
|
start: new Date(subscription.current_period_start * 1000),
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { PrismaService } from '../../prisma';
|
import { PrismaService } from '../../prisma';
|
||||||
import { FeatureKind } from '../features';
|
import { FeatureKind } from '../features';
|
||||||
import { Quota, QuotaType } from './types';
|
import {
|
||||||
|
formatDate,
|
||||||
|
formatSize,
|
||||||
|
getQuotaName,
|
||||||
|
Quota,
|
||||||
|
QuotaType,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QuotaService {
|
export class QuotaService {
|
||||||
@@ -32,11 +38,22 @@ export class QuotaService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.error(userId, quota);
|
||||||
return quota as typeof quota & {
|
return quota as typeof quota & {
|
||||||
feature: Pick<Quota, 'feature' | 'configs'>;
|
feature: Pick<Quota, 'feature' | 'configs'>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHumanReadableQuota(feature: QuotaType, configs: Quota['configs']) {
|
||||||
|
return {
|
||||||
|
name: getQuotaName(feature),
|
||||||
|
blobLimit: formatSize(configs.blobLimit),
|
||||||
|
storageQuota: formatSize(configs.storageQuota),
|
||||||
|
historyPeriod: formatDate(configs.historyPeriod),
|
||||||
|
memberLimit: configs.memberLimit.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// get all user quota records
|
// get all user quota records
|
||||||
async getUserQuotas(userId: string) {
|
async getUserQuotas(userId: string) {
|
||||||
const quotas = await this.prisma.userFeatures.findMany({
|
const quotas = await this.prisma.userFeatures.findMany({
|
||||||
|
|||||||
@@ -5,15 +5,48 @@ export enum QuotaType {
|
|||||||
Quota_ProPlanV1 = 'pro_plan_v1',
|
Quota_ProPlanV1 = 'pro_plan_v1',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum QuotaName {
|
||||||
|
free_plan_v1 = 'Free Plan',
|
||||||
|
pro_plan_v1 = 'Pro Plan',
|
||||||
|
}
|
||||||
|
|
||||||
export type Quota = CommonFeature & {
|
export type Quota = CommonFeature & {
|
||||||
type: FeatureKind.Quota;
|
type: FeatureKind.Quota;
|
||||||
feature: QuotaType;
|
feature: QuotaType;
|
||||||
configs: {
|
configs: {
|
||||||
blobLimit: number;
|
blobLimit: number;
|
||||||
storageQuota: number;
|
storageQuota: number;
|
||||||
|
historyPeriod: number;
|
||||||
|
memberLimit: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const OneKB = 1024;
|
||||||
|
const OneMB = OneKB * OneKB;
|
||||||
|
const OneGB = OneKB * OneMB;
|
||||||
|
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
|
|
||||||
|
export function formatSize(bytes: number, decimals: number = 2): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(OneKB));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(OneKB, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
const OneDay = 1000 * 60 * 60 * 24;
|
||||||
|
|
||||||
|
export function formatDate(ms: number): string {
|
||||||
|
return `${(ms / OneDay).toFixed(0)} days`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQuotaName(quota: QuotaType): string {
|
||||||
|
return QuotaName[quota];
|
||||||
|
}
|
||||||
|
|
||||||
export const Quotas: Quota[] = [
|
export const Quotas: Quota[] = [
|
||||||
{
|
{
|
||||||
feature: QuotaType.Quota_FreePlanV1,
|
feature: QuotaType.Quota_FreePlanV1,
|
||||||
@@ -21,9 +54,13 @@ export const Quotas: Quota[] = [
|
|||||||
version: 1,
|
version: 1,
|
||||||
configs: {
|
configs: {
|
||||||
// single blob limit 10MB
|
// single blob limit 10MB
|
||||||
blobLimit: 10 * 1024 * 1024,
|
blobLimit: 10 * OneMB,
|
||||||
// total blob limit 10GB
|
// total blob limit 10GB
|
||||||
storageQuota: 10 * 1024 * 1024 * 1024,
|
storageQuota: 10 * OneGB,
|
||||||
|
// history period of validity 7 days
|
||||||
|
historyPeriod: 7 * OneDay,
|
||||||
|
// member limit 3
|
||||||
|
memberLimit: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,9 +69,13 @@ export const Quotas: Quota[] = [
|
|||||||
version: 1,
|
version: 1,
|
||||||
configs: {
|
configs: {
|
||||||
// single blob limit 100MB
|
// single blob limit 100MB
|
||||||
blobLimit: 100 * 1024 * 1024,
|
blobLimit: 100 * OneMB,
|
||||||
// total blob limit 100GB
|
// total blob limit 100GB
|
||||||
storageQuota: 100 * 1024 * 1024 * 1024,
|
storageQuota: 100 * OneGB,
|
||||||
|
// history period of validity 30 days
|
||||||
|
historyPeriod: 30 * OneDay,
|
||||||
|
// member limit 10
|
||||||
|
memberLimit: 10,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { FeatureModule } from '../features';
|
import { FeatureModule } from '../features';
|
||||||
|
import { QuotaModule } from '../quota';
|
||||||
import { StorageModule } from '../storage';
|
import { StorageModule } from '../storage';
|
||||||
import { UserResolver } from './resolver';
|
import { UserResolver } from './resolver';
|
||||||
import { UsersService } from './users';
|
import { UsersService } from './users';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [StorageModule, FeatureModule],
|
imports: [StorageModule, FeatureModule, QuotaModule],
|
||||||
providers: [UserResolver, UsersService],
|
providers: [UserResolver, UsersService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|
||||||
export { UserType } from './resolver';
|
export { UserType } from './types';
|
||||||
export { UsersService } from './users';
|
export { UsersService } from './users';
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
Context,
|
Context,
|
||||||
Field,
|
|
||||||
ID,
|
|
||||||
Int,
|
Int,
|
||||||
Mutation,
|
Mutation,
|
||||||
ObjectType,
|
|
||||||
Query,
|
Query,
|
||||||
ResolveField,
|
ResolveField,
|
||||||
Resolver,
|
Resolver,
|
||||||
@@ -26,47 +23,11 @@ import type { FileUpload } from '../../types';
|
|||||||
import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
|
import { Auth, CurrentUser, Public, Publicable } from '../auth/guard';
|
||||||
import { AuthService } from '../auth/service';
|
import { AuthService } from '../auth/service';
|
||||||
import { FeatureManagementService } from '../features';
|
import { FeatureManagementService } from '../features';
|
||||||
|
import { QuotaService } from '../quota';
|
||||||
import { StorageService } from '../storage/storage.service';
|
import { StorageService } from '../storage/storage.service';
|
||||||
|
import { DeleteAccount, RemoveAvatar, UserQuotaType, UserType } from './types';
|
||||||
import { UsersService } from './users';
|
import { UsersService } from './users';
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class UserType implements Partial<User> {
|
|
||||||
@Field(() => ID)
|
|
||||||
id!: string;
|
|
||||||
|
|
||||||
@Field({ description: 'User name' })
|
|
||||||
name!: string;
|
|
||||||
|
|
||||||
@Field({ description: 'User email' })
|
|
||||||
email!: string;
|
|
||||||
|
|
||||||
@Field(() => String, { description: 'User avatar url', nullable: true })
|
|
||||||
avatarUrl: string | null = null;
|
|
||||||
|
|
||||||
@Field(() => Date, { description: 'User email verified', nullable: true })
|
|
||||||
emailVerified: Date | null = null;
|
|
||||||
|
|
||||||
@Field({ description: 'User created date', nullable: true })
|
|
||||||
createdAt!: Date;
|
|
||||||
|
|
||||||
@Field(() => Boolean, {
|
|
||||||
description: 'User password has been set',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
hasPassword?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ObjectType()
|
|
||||||
export class DeleteAccount {
|
|
||||||
@Field()
|
|
||||||
success!: boolean;
|
|
||||||
}
|
|
||||||
@ObjectType()
|
|
||||||
export class RemoveAvatar {
|
|
||||||
@Field()
|
|
||||||
success!: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User resolver
|
* User resolver
|
||||||
* All op rate limit: 10 req/m
|
* All op rate limit: 10 req/m
|
||||||
@@ -80,7 +41,8 @@ export class UserResolver {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly storage: StorageService,
|
private readonly storage: StorageService,
|
||||||
private readonly users: UsersService,
|
private readonly users: UsersService,
|
||||||
private readonly feature: FeatureManagementService
|
private readonly feature: FeatureManagementService,
|
||||||
|
private readonly quota: QuotaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Throttle({
|
@Throttle({
|
||||||
@@ -148,6 +110,24 @@ export class UserResolver {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throttle({ default: { limit: 10, ttl: 60 } })
|
||||||
|
@ResolveField(() => UserQuotaType, { name: 'quota', nullable: true })
|
||||||
|
async getQuota(@CurrentUser() me: User) {
|
||||||
|
const quota = await this.quota.getUserQuota(me.id);
|
||||||
|
const configs = quota.feature.configs;
|
||||||
|
|
||||||
|
return Object.assign(
|
||||||
|
{
|
||||||
|
name: quota.feature.feature,
|
||||||
|
humanReadable: this.quota.getHumanReadableQuota(
|
||||||
|
quota.feature.feature,
|
||||||
|
configs
|
||||||
|
),
|
||||||
|
},
|
||||||
|
configs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Throttle({ default: { limit: 10, ttl: 60 } })
|
@Throttle({ default: { limit: 10, ttl: 60 } })
|
||||||
@ResolveField(() => Int, {
|
@ResolveField(() => Int, {
|
||||||
name: 'invoiceCount',
|
name: 'invoiceCount',
|
||||||
|
|||||||
79
packages/backend/server/src/modules/users/types.ts
Normal file
79
packages/backend/server/src/modules/users/types.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Field, Float, ID, ObjectType } from '@nestjs/graphql';
|
||||||
|
import type { User } from '@prisma/client';
|
||||||
|
|
||||||
|
@ObjectType('UserQuotaHumanReadable')
|
||||||
|
export class UserQuotaHumanReadableType {
|
||||||
|
@Field({ name: 'name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Field({ name: 'blobLimit' })
|
||||||
|
blobLimit!: string;
|
||||||
|
|
||||||
|
@Field({ name: 'storageQuota' })
|
||||||
|
storageQuota!: string;
|
||||||
|
|
||||||
|
@Field({ name: 'historyPeriod' })
|
||||||
|
historyPeriod!: string;
|
||||||
|
|
||||||
|
@Field({ name: 'memberLimit' })
|
||||||
|
memberLimit!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType('UserQuota')
|
||||||
|
export class UserQuotaType {
|
||||||
|
@Field({ name: 'name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Field(() => Float, { name: 'blobLimit' })
|
||||||
|
blobLimit!: number;
|
||||||
|
|
||||||
|
@Field(() => Float, { name: 'storageQuota' })
|
||||||
|
storageQuota!: number;
|
||||||
|
|
||||||
|
@Field(() => Float, { name: 'historyPeriod' })
|
||||||
|
historyPeriod!: number;
|
||||||
|
|
||||||
|
@Field({ name: 'memberLimit' })
|
||||||
|
memberLimit!: number;
|
||||||
|
|
||||||
|
@Field({ name: 'humanReadable' })
|
||||||
|
humanReadable!: UserQuotaHumanReadableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class UserType implements Partial<User> {
|
||||||
|
@Field(() => ID)
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Field({ description: 'User name' })
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@Field({ description: 'User email' })
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@Field(() => String, { description: 'User avatar url', nullable: true })
|
||||||
|
avatarUrl: string | null = null;
|
||||||
|
|
||||||
|
@Field(() => Date, { description: 'User email verified', nullable: true })
|
||||||
|
emailVerified: Date | null = null;
|
||||||
|
|
||||||
|
@Field({ description: 'User created date', nullable: true })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@Field(() => Boolean, {
|
||||||
|
description: 'User password has been set',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
hasPassword?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class DeleteAccount {
|
||||||
|
@Field()
|
||||||
|
success!: boolean;
|
||||||
|
}
|
||||||
|
@ObjectType()
|
||||||
|
export class RemoveAvatar {
|
||||||
|
@Field()
|
||||||
|
success!: boolean;
|
||||||
|
}
|
||||||
@@ -43,8 +43,7 @@ import { Auth, CurrentUser, Public } from '../auth';
|
|||||||
import { MailService } from '../auth/mailer';
|
import { MailService } from '../auth/mailer';
|
||||||
import { AuthService } from '../auth/service';
|
import { AuthService } from '../auth/service';
|
||||||
import { QuotaManagementService } from '../quota';
|
import { QuotaManagementService } from '../quota';
|
||||||
import { UsersService } from '../users';
|
import { UsersService, UserType } from '../users';
|
||||||
import { UserType } from '../users/resolver';
|
|
||||||
import { PermissionService, PublicPageMode } from './permission';
|
import { PermissionService, PublicPageMode } from './permission';
|
||||||
import { Permission } from './types';
|
import { Permission } from './types';
|
||||||
import { defaultWorkspaceAvatar } from './utils';
|
import { defaultWorkspaceAvatar } from './utils';
|
||||||
|
|||||||
@@ -10,6 +10,23 @@ type ServerConfigType {
|
|||||||
flavor: String!
|
flavor: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserQuotaHumanReadable {
|
||||||
|
name: String!
|
||||||
|
blobLimit: String!
|
||||||
|
storageQuota: String!
|
||||||
|
historyPeriod: String!
|
||||||
|
memberLimit: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserQuota {
|
||||||
|
name: String!
|
||||||
|
blobLimit: Float!
|
||||||
|
storageQuota: Float!
|
||||||
|
historyPeriod: Float!
|
||||||
|
memberLimit: Int!
|
||||||
|
humanReadable: UserQuotaHumanReadable!
|
||||||
|
}
|
||||||
|
|
||||||
type UserType {
|
type UserType {
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
@@ -31,6 +48,7 @@ type UserType {
|
|||||||
"""User password has been set"""
|
"""User password has been set"""
|
||||||
hasPassword: Boolean
|
hasPassword: Boolean
|
||||||
token: TokenType!
|
token: TokenType!
|
||||||
|
quota: UserQuota
|
||||||
|
|
||||||
"""Get user invoice count"""
|
"""Get user invoice count"""
|
||||||
invoiceCount: Int!
|
invoiceCount: Int!
|
||||||
|
|||||||
@@ -14,10 +14,16 @@ import {
|
|||||||
|
|
||||||
import { CacheModule } from '../src/cache';
|
import { CacheModule } from '../src/cache';
|
||||||
import { Config, ConfigModule } from '../src/config';
|
import { Config, ConfigModule } from '../src/config';
|
||||||
|
import {
|
||||||
|
collectMigrations,
|
||||||
|
RevertCommand,
|
||||||
|
RunCommand,
|
||||||
|
} from '../src/data/commands/run';
|
||||||
import { EventModule } from '../src/event';
|
import { EventModule } from '../src/event';
|
||||||
import { DocManager, DocModule } from '../src/modules/doc';
|
import { DocManager, DocModule } from '../src/modules/doc';
|
||||||
|
import { QuotaModule } from '../src/modules/quota';
|
||||||
import { PrismaModule, PrismaService } from '../src/prisma';
|
import { PrismaModule, PrismaService } from '../src/prisma';
|
||||||
import { flushDB } from './utils';
|
import { FakeStorageModule, flushDB } from './utils';
|
||||||
|
|
||||||
const createModule = () => {
|
const createModule = () => {
|
||||||
return Test.createTestingModule({
|
return Test.createTestingModule({
|
||||||
@@ -25,8 +31,12 @@ const createModule = () => {
|
|||||||
PrismaModule,
|
PrismaModule,
|
||||||
CacheModule,
|
CacheModule,
|
||||||
EventModule,
|
EventModule,
|
||||||
|
QuotaModule,
|
||||||
|
FakeStorageModule.forRoot(),
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot(),
|
||||||
DocModule,
|
DocModule,
|
||||||
|
RevertCommand,
|
||||||
|
RunCommand,
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
};
|
};
|
||||||
@@ -45,6 +55,13 @@ test.beforeEach(async () => {
|
|||||||
app = m.createNestApplication();
|
app = m.createNestApplication();
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
await app.init();
|
await app.init();
|
||||||
|
|
||||||
|
// init features
|
||||||
|
const run = m.get(RunCommand);
|
||||||
|
const revert = m.get(RevertCommand);
|
||||||
|
const migrations = await collectMigrations();
|
||||||
|
await Promise.allSettled(migrations.map(m => revert.run([m.name])));
|
||||||
|
await run.run();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach.always(async () => {
|
test.afterEach.always(async () => {
|
||||||
|
|||||||
@@ -57,14 +57,10 @@ test.beforeEach(async t => {
|
|||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
const quota = module.get(FeatureService);
|
|
||||||
const storageQuota = module.get(FeatureManagementService);
|
|
||||||
const auth = module.get(AuthService);
|
|
||||||
|
|
||||||
t.context.app = module;
|
t.context.app = module;
|
||||||
t.context.feature = quota;
|
t.context.auth = module.get(AuthService);
|
||||||
t.context.early_access = storageQuota;
|
t.context.feature = module.get(FeatureService);
|
||||||
t.context.auth = auth;
|
t.context.early_access = module.get(FeatureManagementService);
|
||||||
|
|
||||||
// init features
|
// init features
|
||||||
await initFeatureConfigs(module);
|
await initFeatureConfigs(module);
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import * as Sinon from 'sinon';
|
|||||||
import { ConfigModule } from '../src/config';
|
import { ConfigModule } from '../src/config';
|
||||||
import type { EventPayload } from '../src/event';
|
import type { EventPayload } from '../src/event';
|
||||||
import { DocHistoryManager } from '../src/modules/doc';
|
import { DocHistoryManager } from '../src/modules/doc';
|
||||||
|
import { QuotaModule } from '../src/modules/quota';
|
||||||
import { PrismaModule, PrismaService } from '../src/prisma';
|
import { PrismaModule, PrismaService } from '../src/prisma';
|
||||||
import { flushDB } from './utils';
|
import { FakeStorageModule, flushDB } from './utils';
|
||||||
|
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
let m: TestingModule;
|
let m: TestingModule;
|
||||||
@@ -20,7 +21,13 @@ let db: PrismaService;
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await flushDB();
|
await flushDB();
|
||||||
m = await Test.createTestingModule({
|
m = await Test.createTestingModule({
|
||||||
imports: [PrismaModule, ScheduleModule.forRoot(), ConfigModule.forRoot()],
|
imports: [
|
||||||
|
PrismaModule,
|
||||||
|
QuotaModule,
|
||||||
|
FakeStorageModule.forRoot(),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
ConfigModule.forRoot(),
|
||||||
|
],
|
||||||
providers: [DocHistoryManager],
|
providers: [DocHistoryManager],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@@ -277,8 +284,8 @@ test('should be able to recover from history', async t => {
|
|||||||
t.is(history2.timestamp.getTime(), snapshot.updatedAt.getTime());
|
t.is(history2.timestamp.getTime(), snapshot.updatedAt.getTime());
|
||||||
|
|
||||||
// new history data force created with snapshot state before recovered
|
// new history data force created with snapshot state before recovered
|
||||||
t.deepEqual(history2?.blob, Buffer.from([1, 1]));
|
t.deepEqual(history2.blob, Buffer.from([1, 1]));
|
||||||
t.deepEqual(history2?.state, Buffer.from([1, 1]));
|
t.deepEqual(history2.state, Buffer.from([1, 1]));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should be able to cleanup expired history', async t => {
|
test('should be able to cleanup expired history', async t => {
|
||||||
|
|||||||
@@ -16,9 +16,8 @@ import {
|
|||||||
QuotaType,
|
QuotaType,
|
||||||
} from '../src/modules/quota';
|
} from '../src/modules/quota';
|
||||||
import { PrismaModule } from '../src/prisma';
|
import { PrismaModule } from '../src/prisma';
|
||||||
import { StorageModule } from '../src/storage';
|
|
||||||
import { RateLimiterModule } from '../src/throttler';
|
import { RateLimiterModule } from '../src/throttler';
|
||||||
import { initFeatureConfigs } from './utils';
|
import { FakeStorageModule, initFeatureConfigs } from './utils';
|
||||||
|
|
||||||
const test = ava as TestFn<{
|
const test = ava as TestFn<{
|
||||||
auth: AuthService;
|
auth: AuthService;
|
||||||
@@ -47,10 +46,10 @@ test.beforeEach(async t => {
|
|||||||
host: 'example.org',
|
host: 'example.org',
|
||||||
https: true,
|
https: true,
|
||||||
}),
|
}),
|
||||||
StorageModule.forRoot(),
|
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
QuotaModule,
|
QuotaModule,
|
||||||
|
FakeStorageModule.forRoot(),
|
||||||
RateLimiterModule,
|
RateLimiterModule,
|
||||||
RevertCommand,
|
RevertCommand,
|
||||||
RunCommand,
|
RunCommand,
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
|
||||||
import { AppModule } from '../src/app';
|
import { AppModule } from '../src/app';
|
||||||
import { currentUser, signUp } from './utils';
|
import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
||||||
|
import { currentUser, initFeatureConfigs, signUp } from './utils';
|
||||||
|
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ test.beforeEach(async () => {
|
|||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
const module = await Test.createTestingModule({
|
const module = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
|
providers: [RevertCommand, RunCommand],
|
||||||
}).compile();
|
}).compile();
|
||||||
app = module.createNestApplication();
|
app = module.createNestApplication();
|
||||||
app.use(
|
app.use(
|
||||||
@@ -30,6 +32,9 @@ test.beforeEach(async () => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
await app.init();
|
await app.init();
|
||||||
|
|
||||||
|
// init features
|
||||||
|
await initFeatureConfigs(module);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.afterEach.always(async () => {
|
test.afterEach.always(async () => {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
|
||||||
import type { INestApplication } from '@nestjs/common';
|
import type {
|
||||||
|
DynamicModule,
|
||||||
|
FactoryProvider,
|
||||||
|
INestApplication,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { TestingModule } from '@nestjs/testing';
|
import { TestingModule } from '@nestjs/testing';
|
||||||
import { hashSync } from '@node-rs/argon2';
|
import { hashSync } from '@node-rs/argon2';
|
||||||
import { PrismaClient, type User } from '@prisma/client';
|
import { PrismaClient, type User } from '@prisma/client';
|
||||||
@@ -10,6 +14,7 @@ import { RevertCommand, RunCommand } from '../src/data/commands/run';
|
|||||||
import type { TokenType } from '../src/modules/auth';
|
import type { TokenType } from '../src/modules/auth';
|
||||||
import type { UserType } from '../src/modules/users';
|
import type { UserType } from '../src/modules/users';
|
||||||
import type { InvitationType, WorkspaceType } from '../src/modules/workspaces';
|
import type { InvitationType, WorkspaceType } from '../src/modules/workspaces';
|
||||||
|
import { StorageProvide } from '../src/storage';
|
||||||
|
|
||||||
const gql = '/graphql';
|
const gql = '/graphql';
|
||||||
|
|
||||||
@@ -563,6 +568,24 @@ export class FakePrisma {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FakeStorageModule {
|
||||||
|
static forRoot(): DynamicModule {
|
||||||
|
const storageProvider: FactoryProvider = {
|
||||||
|
provide: StorageProvide,
|
||||||
|
useFactory: async () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
global: true,
|
||||||
|
module: FakeStorageModule,
|
||||||
|
providers: [storageProvider],
|
||||||
|
exports: [storageProvider],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function initFeatureConfigs(module: TestingModule) {
|
export async function initFeatureConfigs(module: TestingModule) {
|
||||||
const run = module.get(RunCommand);
|
const run = module.get(RunCommand);
|
||||||
const revert = module.get(RevertCommand);
|
const revert = module.get(RevertCommand);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { Menu, MenuItem } from '@affine/component/ui/menu';
|
|||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Permission, SubscriptionPlan } from '@affine/graphql';
|
import { Permission } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowRightBigIcon, MoreVerticalIcon } from '@blocksuite/icons';
|
import { ArrowRightBigIcon, MoreVerticalIcon } from '@blocksuite/icons';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -37,17 +37,11 @@ import { useInviteMember } from '../../../hooks/affine/use-invite-member';
|
|||||||
import { useMemberCount } from '../../../hooks/affine/use-member-count';
|
import { useMemberCount } from '../../../hooks/affine/use-member-count';
|
||||||
import { type Member, useMembers } from '../../../hooks/affine/use-members';
|
import { type Member, useMembers } from '../../../hooks/affine/use-members';
|
||||||
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
|
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
|
||||||
import { useUserSubscription } from '../../../hooks/use-subscription';
|
import { useUserQuota } from '../../../hooks/use-quota';
|
||||||
import { AffineErrorBoundary } from '../affine-error-boundary';
|
import { AffineErrorBoundary } from '../affine-error-boundary';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
enum MemberLimitCount {
|
|
||||||
Free = '3',
|
|
||||||
Pro = '10',
|
|
||||||
Other = '?',
|
|
||||||
}
|
|
||||||
|
|
||||||
const COUNT_PER_PAGE = 8;
|
const COUNT_PER_PAGE = 8;
|
||||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||||
upgradable: boolean;
|
upgradable: boolean;
|
||||||
@@ -148,23 +142,17 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
});
|
});
|
||||||
}, [setSettingModalAtom]);
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
const [subscription] = useUserSubscription();
|
const quota = useUserQuota();
|
||||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
|
||||||
const memberLimit = useMemo(() => {
|
|
||||||
if (plan === SubscriptionPlan.Free) {
|
|
||||||
return MemberLimitCount.Free;
|
|
||||||
}
|
|
||||||
if (plan === SubscriptionPlan.Pro) {
|
|
||||||
return MemberLimitCount.Pro;
|
|
||||||
}
|
|
||||||
return MemberLimitCount.Other;
|
|
||||||
}, [plan]);
|
|
||||||
const desc = useMemo(() => {
|
const desc = useMemo(() => {
|
||||||
|
if (!quota) return null;
|
||||||
|
|
||||||
|
const humanReadable = quota.humanReadable;
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{t['com.affine.payment.member.description']({
|
{t['com.affine.payment.member.description']({
|
||||||
planName: plan,
|
planName: humanReadable.name,
|
||||||
memberLimit,
|
memberLimit: humanReadable.memberLimit,
|
||||||
})}
|
})}
|
||||||
{upgradable ? (
|
{upgradable ? (
|
||||||
<>
|
<>
|
||||||
@@ -179,7 +167,7 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}, [handleUpgrade, memberLimit, plan, t, upgradable]);
|
}, [handleUpgrade, quota, t, upgradable]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
10
packages/frontend/core/src/hooks/use-quota.ts
Normal file
10
packages/frontend/core/src/hooks/use-quota.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { quotaQuery } from '@affine/graphql';
|
||||||
|
import { useQuery } from '@affine/workspace/affine/gql';
|
||||||
|
|
||||||
|
export const useUserQuota = () => {
|
||||||
|
const { data } = useQuery({
|
||||||
|
query: quotaQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.currentUser?.quota || null;
|
||||||
|
};
|
||||||
@@ -508,6 +508,32 @@ mutation publishPage($workspaceId: String!, $pageId: String!, $mode: PublicPageM
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const quotaQuery = {
|
||||||
|
id: 'quotaQuery' as const,
|
||||||
|
operationName: 'quota',
|
||||||
|
definitionName: 'currentUser',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query quota {
|
||||||
|
currentUser {
|
||||||
|
quota {
|
||||||
|
name
|
||||||
|
blobLimit
|
||||||
|
storageQuota
|
||||||
|
historyPeriod
|
||||||
|
memberLimit
|
||||||
|
humanReadable {
|
||||||
|
name
|
||||||
|
blobLimit
|
||||||
|
storageQuota
|
||||||
|
historyPeriod
|
||||||
|
memberLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const recoverDocMutation = {
|
export const recoverDocMutation = {
|
||||||
id: 'recoverDocMutation' as const,
|
id: 'recoverDocMutation' as const,
|
||||||
operationName: 'recoverDoc',
|
operationName: 'recoverDoc',
|
||||||
|
|||||||
18
packages/frontend/graphql/src/graphql/quota.gql
Normal file
18
packages/frontend/graphql/src/graphql/quota.gql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
query quota {
|
||||||
|
currentUser {
|
||||||
|
quota {
|
||||||
|
name
|
||||||
|
blobLimit
|
||||||
|
storageQuota
|
||||||
|
historyPeriod
|
||||||
|
memberLimit
|
||||||
|
humanReadable {
|
||||||
|
name
|
||||||
|
blobLimit
|
||||||
|
storageQuota
|
||||||
|
historyPeriod
|
||||||
|
memberLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -499,6 +499,31 @@ export type PublishPageMutation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type QuotaQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
|
export type QuotaQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
currentUser: {
|
||||||
|
__typename?: 'UserType';
|
||||||
|
quota: {
|
||||||
|
__typename?: 'UserQuota';
|
||||||
|
name: string;
|
||||||
|
blobLimit: number;
|
||||||
|
storageQuota: number;
|
||||||
|
historyPeriod: number;
|
||||||
|
memberLimit: number;
|
||||||
|
humanReadable: {
|
||||||
|
__typename?: 'UserQuotaHumanReadable';
|
||||||
|
name: string;
|
||||||
|
blobLimit: string;
|
||||||
|
storageQuota: string;
|
||||||
|
historyPeriod: string;
|
||||||
|
memberLimit: string;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type RecoverDocMutationVariables = Exact<{
|
export type RecoverDocMutationVariables = Exact<{
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
docId: Scalars['String']['input'];
|
docId: Scalars['String']['input'];
|
||||||
@@ -819,6 +844,11 @@ export type Queries =
|
|||||||
variables: PricesQueryVariables;
|
variables: PricesQueryVariables;
|
||||||
response: PricesQuery;
|
response: PricesQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'quotaQuery';
|
||||||
|
variables: QuotaQueryVariables;
|
||||||
|
response: QuotaQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'serverConfigQuery';
|
name: 'serverConfigQuery';
|
||||||
variables: ServerConfigQueryVariables;
|
variables: ServerConfigQueryVariables;
|
||||||
|
|||||||
Reference in New Issue
Block a user