fix: hide exact quota values from account response #16
@@ -44,3 +44,6 @@ This repository is a TypeScript monorepo for `nproxy`, a crypto-subscription ima
|
|||||||
- If you change deployment assumptions, update `docs/ops/deployment.md`.
|
- If you change deployment assumptions, update `docs/ops/deployment.md`.
|
||||||
- If you change Telegram admin auth, update `docs/ops/telegram-pairing.md`.
|
- If you change Telegram admin auth, update `docs/ops/telegram-pairing.md`.
|
||||||
- If you change failover, cooldown, or balance logic, update `docs/ops/provider-key-pool.md`.
|
- If you change failover, cooldown, or balance logic, update `docs/ops/provider-key-pool.md`.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
- At the end of each completed task, create a PR for the changes unless the user explicitly says not to.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"test": "node --test dist/**/*.test.js",
|
||||||
"start": "node dist/main.js"
|
"start": "node dist/main.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
65
apps/web/src/account-response.test.ts
Normal file
65
apps/web/src/account-response.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { serializePublicAccountOverview } from "./account-response.js";
|
||||||
|
|
||||||
|
test("serializePublicAccountOverview exposes only approximate quota fields", () => {
|
||||||
|
const response = serializePublicAccountOverview({
|
||||||
|
user: {
|
||||||
|
id: "user_1",
|
||||||
|
email: "user@example.com",
|
||||||
|
isAdmin: false,
|
||||||
|
createdAt: new Date("2026-03-10T12:00:00.000Z"),
|
||||||
|
},
|
||||||
|
subscription: {
|
||||||
|
id: "sub_1",
|
||||||
|
status: "active",
|
||||||
|
renewsManually: true,
|
||||||
|
activatedAt: new Date("2026-03-10T12:00:00.000Z"),
|
||||||
|
currentPeriodStart: new Date("2026-03-10T12:00:00.000Z"),
|
||||||
|
currentPeriodEnd: new Date("2026-04-09T12:00:00.000Z"),
|
||||||
|
plan: {
|
||||||
|
id: "plan_1",
|
||||||
|
code: "basic",
|
||||||
|
displayName: "Basic",
|
||||||
|
monthlyPriceUsd: 29,
|
||||||
|
billingCurrency: "USDT",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
quota: {
|
||||||
|
approximateBucket: 80,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(response, {
|
||||||
|
user: {
|
||||||
|
id: "user_1",
|
||||||
|
email: "user@example.com",
|
||||||
|
isAdmin: false,
|
||||||
|
createdAt: "2026-03-10T12:00:00.000Z",
|
||||||
|
},
|
||||||
|
subscription: {
|
||||||
|
id: "sub_1",
|
||||||
|
status: "active",
|
||||||
|
renewsManually: true,
|
||||||
|
activatedAt: "2026-03-10T12:00:00.000Z",
|
||||||
|
currentPeriodStart: "2026-03-10T12:00:00.000Z",
|
||||||
|
currentPeriodEnd: "2026-04-09T12:00:00.000Z",
|
||||||
|
plan: {
|
||||||
|
id: "plan_1",
|
||||||
|
code: "basic",
|
||||||
|
displayName: "Basic",
|
||||||
|
monthlyPriceUsd: 29,
|
||||||
|
billingCurrency: "USDT",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
quota: {
|
||||||
|
approximateBucket: 80,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal("usedSuccessfulRequests" in (response.quota ?? {}), false);
|
||||||
|
assert.equal("monthlyRequestLimit" in (response.quota ?? {}), false);
|
||||||
|
assert.equal("monthlyRequestLimit" in (response.subscription?.plan ?? {}), false);
|
||||||
|
});
|
||||||
71
apps/web/src/account-response.ts
Normal file
71
apps/web/src/account-response.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
export interface PublicAccountOverviewLike {
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
};
|
||||||
|
subscription: {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
renewsManually: boolean;
|
||||||
|
activatedAt?: Date;
|
||||||
|
currentPeriodStart?: Date;
|
||||||
|
currentPeriodEnd?: Date;
|
||||||
|
canceledAt?: Date;
|
||||||
|
plan: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
displayName: string;
|
||||||
|
monthlyPriceUsd: number;
|
||||||
|
billingCurrency: string;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
} | null;
|
||||||
|
quota: {
|
||||||
|
approximateBucket: number;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializePublicAccountOverview(overview: PublicAccountOverviewLike) {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: overview.user.id,
|
||||||
|
email: overview.user.email,
|
||||||
|
isAdmin: overview.user.isAdmin,
|
||||||
|
createdAt: overview.user.createdAt.toISOString(),
|
||||||
|
},
|
||||||
|
subscription: overview.subscription
|
||||||
|
? {
|
||||||
|
id: overview.subscription.id,
|
||||||
|
status: overview.subscription.status,
|
||||||
|
renewsManually: overview.subscription.renewsManually,
|
||||||
|
...(overview.subscription.activatedAt
|
||||||
|
? { activatedAt: overview.subscription.activatedAt.toISOString() }
|
||||||
|
: {}),
|
||||||
|
...(overview.subscription.currentPeriodStart
|
||||||
|
? { currentPeriodStart: overview.subscription.currentPeriodStart.toISOString() }
|
||||||
|
: {}),
|
||||||
|
...(overview.subscription.currentPeriodEnd
|
||||||
|
? { currentPeriodEnd: overview.subscription.currentPeriodEnd.toISOString() }
|
||||||
|
: {}),
|
||||||
|
...(overview.subscription.canceledAt
|
||||||
|
? { canceledAt: overview.subscription.canceledAt.toISOString() }
|
||||||
|
: {}),
|
||||||
|
plan: {
|
||||||
|
id: overview.subscription.plan.id,
|
||||||
|
code: overview.subscription.plan.code,
|
||||||
|
displayName: overview.subscription.plan.displayName,
|
||||||
|
monthlyPriceUsd: overview.subscription.plan.monthlyPriceUsd,
|
||||||
|
billingCurrency: overview.subscription.plan.billingCurrency,
|
||||||
|
isActive: overview.subscription.plan.isActive,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
quota: overview.quota
|
||||||
|
? {
|
||||||
|
approximateBucket: overview.quota.approximateBucket,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
createGenerationRequest,
|
createGenerationRequest,
|
||||||
type CreateGenerationRequestInput,
|
type CreateGenerationRequestInput,
|
||||||
} from "@nproxy/domain";
|
} from "@nproxy/domain";
|
||||||
|
import { serializePublicAccountOverview } from "./account-response.js";
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
||||||
@@ -159,7 +160,7 @@ const server = createServer(async (request, response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendJson(response, 200, serializeAccountOverview(overview));
|
sendJson(response, 200, serializePublicAccountOverview(overview));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,63 +494,6 @@ function serializeUserSession(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeAccountOverview(overview: {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
};
|
|
||||||
subscription: {
|
|
||||||
id: string;
|
|
||||||
status: string;
|
|
||||||
renewsManually: boolean;
|
|
||||||
activatedAt?: Date;
|
|
||||||
currentPeriodStart?: Date;
|
|
||||||
currentPeriodEnd?: Date;
|
|
||||||
canceledAt?: Date;
|
|
||||||
plan: {
|
|
||||||
id: string;
|
|
||||||
code: string;
|
|
||||||
displayName: string;
|
|
||||||
monthlyRequestLimit: number;
|
|
||||||
monthlyPriceUsd: number;
|
|
||||||
billingCurrency: string;
|
|
||||||
isActive: boolean;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
quota: {
|
|
||||||
approximateBucket: number;
|
|
||||||
usedSuccessfulRequests: number;
|
|
||||||
monthlyRequestLimit: number;
|
|
||||||
} | null;
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
user: serializeAuthenticatedUser(overview.user),
|
|
||||||
subscription: overview.subscription
|
|
||||||
? {
|
|
||||||
id: overview.subscription.id,
|
|
||||||
status: overview.subscription.status,
|
|
||||||
renewsManually: overview.subscription.renewsManually,
|
|
||||||
...(overview.subscription.activatedAt
|
|
||||||
? { activatedAt: overview.subscription.activatedAt.toISOString() }
|
|
||||||
: {}),
|
|
||||||
...(overview.subscription.currentPeriodStart
|
|
||||||
? { currentPeriodStart: overview.subscription.currentPeriodStart.toISOString() }
|
|
||||||
: {}),
|
|
||||||
...(overview.subscription.currentPeriodEnd
|
|
||||||
? { currentPeriodEnd: overview.subscription.currentPeriodEnd.toISOString() }
|
|
||||||
: {}),
|
|
||||||
...(overview.subscription.canceledAt
|
|
||||||
? { canceledAt: overview.subscription.canceledAt.toISOString() }
|
|
||||||
: {}),
|
|
||||||
plan: overview.subscription.plan,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
quota: overview.quota,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeBillingInvoice(invoice: {
|
function serializeBillingInvoice(invoice: {
|
||||||
id: string;
|
id: string;
|
||||||
subscriptionId?: string;
|
subscriptionId?: string;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ export interface UserAccountOverview {
|
|||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
monthlyRequestLimit: number;
|
|
||||||
monthlyPriceUsd: number;
|
monthlyPriceUsd: number;
|
||||||
billingCurrency: string;
|
billingCurrency: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -30,8 +29,6 @@ export interface UserAccountOverview {
|
|||||||
} | null;
|
} | null;
|
||||||
quota: {
|
quota: {
|
||||||
approximateBucket: QuotaBucket;
|
approximateBucket: QuotaBucket;
|
||||||
usedSuccessfulRequests: number;
|
|
||||||
monthlyRequestLimit: number;
|
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +92,6 @@ export function createPrismaAccountStore(database: PrismaClient = defaultPrisma)
|
|||||||
id: subscription.plan.id,
|
id: subscription.plan.id,
|
||||||
code: subscription.plan.code,
|
code: subscription.plan.code,
|
||||||
displayName: subscription.plan.displayName,
|
displayName: subscription.plan.displayName,
|
||||||
monthlyRequestLimit: subscription.plan.monthlyRequestLimit,
|
|
||||||
monthlyPriceUsd: decimalToNumber(subscription.plan.monthlyPriceUsd),
|
monthlyPriceUsd: decimalToNumber(subscription.plan.monthlyPriceUsd),
|
||||||
billingCurrency: subscription.plan.billingCurrency,
|
billingCurrency: subscription.plan.billingCurrency,
|
||||||
isActive: subscription.plan.isActive,
|
isActive: subscription.plan.isActive,
|
||||||
@@ -140,7 +136,5 @@ async function buildQuotaSnapshot(
|
|||||||
used: usedSuccessfulRequests,
|
used: usedSuccessfulRequests,
|
||||||
limit: input.monthlyRequestLimit,
|
limit: input.monthlyRequestLimit,
|
||||||
}),
|
}),
|
||||||
usedSuccessfulRequests,
|
|
||||||
monthlyRequestLimit: input.monthlyRequestLimit,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user