bypass subscription in test env
All checks were successful
Test - Build & Deploy to Scaleway / build-and-push-images-lecoffre (push) Successful in 1m44s
Test - Build & Deploy to Scaleway / deploy-back-lecoffre (push) Successful in 4s
Test - Build & Deploy to Scaleway / deploy-cron-lecoffre (push) Successful in 3s

This commit is contained in:
Sosthene 2025-07-30 19:19:53 +02:00
parent bbc0c6ddcd
commit 5f50e13a50
6 changed files with 330 additions and 25 deletions

84
SUBSCRIPTION_BYPASS.md Normal file
View File

@ -0,0 +1,84 @@
# Subscription Bypass for Development/Test Environments
## Overview
The subscription system has been modified to automatically bypass subscription checks in development and test environments. This allows developers to work without needing to set up complex subscription data or Stripe integrations during development.
## How it Works
### Environment Detection
The system automatically detects the environment using the `ENV` environment variable:
- `dev` - Development environment
- `test` - Test environment
- Any other value - Production environment
### Bypass Behavior
When running in `dev` or `test` environments:
1. **All Users**: The `isUserSubscribed()` method automatically returns `true` for all users
2. **Admin Users**: Admin and super-admin users get additional subscription management rules automatically assigned
3. **Logging**: All bypass actions are logged with `[DEV/TEST]` prefix for easy identification
### Production Behavior
In production environments (any ENV value other than `dev` or `test`):
- Subscription checks work normally
- No bypassing occurs
- All existing subscription logic is preserved
## Configuration
### Environment Variables
Set the `ENV` environment variable in your environment files:
```bash
# .env.dev
ENV=dev
# .env.test
ENV=test
# .env.prod
ENV=prod
```
### Code Structure
The bypass logic is centralized in:
- `src/common/config/SubscriptionBypass.ts` - Main configuration class
- `src/services/admin/SubscriptionsService/SubscriptionsService.ts.ts` - Service layer bypass
- `src/app/api/idnot/UserController.ts` - Controller layer bypass
## Logging
When subscription checks are bypassed, you'll see logs like:
```
[DEV/TEST] Bypassing subscription check for user abc123 in office xyz789 (ENV: dev) - isUserSubscribed method
[DEV/TEST] Bypassing subscription check for user abc123 in office xyz789 (ENV: dev) - admin user login
```
## Benefits
1. **Faster Development**: No need to set up Stripe subscriptions for testing
2. **Simplified Testing**: Tests can run without subscription dependencies
3. **Production Safety**: No risk of accidentally bypassing subscriptions in production
4. **Clear Logging**: Easy to identify when bypasses are happening
5. **Maintainable**: Centralized configuration makes it easy to modify bypass behavior
## Security Considerations
- The bypass only works in `dev` and `test` environments
- Production environments are completely unaffected
- All bypass actions are logged for audit purposes
- The bypass is implemented at the service layer, ensuring consistency across the application
## Troubleshooting
If subscription checks are still failing in development:
1. Check that `ENV=dev` or `ENV=test` is set in your environment
2. Verify the environment variable is being loaded correctly
3. Check the logs for `[DEV/TEST]` messages to confirm bypass is working
4. Ensure the `SubscriptionBypass` service is being injected correctly

View File

@ -9,6 +9,7 @@ import User, { RulesGroup } from "le-coffre-resources/dist/Admin";
import UsersService from "@Services/super-admin/UsersService/UsersService";
import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts";
import RulesGroupsService from "@Services/admin/RulesGroupsService/RulesGroupsService";
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
@Controller()
@Service()
@ -19,6 +20,7 @@ export default class UserController extends ApiController {
private userService: UsersService,
private subscriptionsService: SubscriptionsService,
private rulesGroupsService: RulesGroupsService,
private subscriptionBypass: SubscriptionBypass,
) {
super();
}
@ -100,21 +102,43 @@ export default class UserController extends ApiController {
return;
}
if (!isSubscribed && (userHydrated.role?.name === "admin" || userHydrated.role?.name === "super-admin")) {
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
include: { rules: true },
});
// In development/test environments, bypass admin role subscription requirements
if (this.subscriptionBypass.shouldBypassSubscriptionChecks()) {
if (!isSubscribed && (userHydrated.role?.name === "admin" || userHydrated.role?.name === "super-admin")) {
this.subscriptionBypass.logBypass(user.uid, userHydrated.office_membership?.uid!, "admin user login");
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
include: { rules: true },
});
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
strategy: "excludeAll",
});
if (!manageSubscriptionRules[0]) return;
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
strategy: "excludeAll",
});
if (!manageSubscriptionRules[0]) return;
payload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || [];
payload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || [];
isSubscribed = true;
isSubscribed = true;
}
} else {
// Production logic - only bypass for admin users
if (!isSubscribed && (userHydrated.role?.name === "admin" || userHydrated.role?.name === "super-admin")) {
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
include: { rules: true },
});
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
strategy: "excludeAll",
});
if (!manageSubscriptionRules[0]) return;
payload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || [];
isSubscribed = true;
}
}
if (!isSubscribed) {
@ -162,20 +186,42 @@ export default class UserController extends ApiController {
)) as IUserJwtPayload;
let isSubscribed = await this.subscriptionsService.isUserSubscribed(newUserPayload.userId, newUserPayload.office_Id);
if (!isSubscribed && (newUserPayload.role === "admin" || newUserPayload.role === "super-admin")) {
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
include: { rules: true },
});
// In development/test environments, bypass admin role subscription requirements
if (this.subscriptionBypass.shouldBypassSubscriptionChecks()) {
if (!isSubscribed && (newUserPayload.role === "admin" || newUserPayload.role === "super-admin")) {
this.subscriptionBypass.logBypass(newUserPayload.userId, newUserPayload.office_Id, "admin user refresh token");
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
include: { rules: true },
});
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
strategy: "excludeAll",
});
if (!manageSubscriptionRules[0]) return;
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
strategy: "excludeAll",
});
if (!manageSubscriptionRules[0]) return;
newUserPayload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || [];
newUserPayload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || [];
isSubscribed = true;
isSubscribed = true;
}
} else {
// Production logic
if (!isSubscribed && (newUserPayload.role === "admin" || newUserPayload.role === "super-admin")) {
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
include: { rules: true },
});
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
strategy: "excludeAll",
});
if (!manageSubscriptionRules[0]) return;
newUserPayload.rules = manageSubscriptionRules[0].rules!.map((rule) => rule.name) || [];
isSubscribed = true;
}
}
delete newUserPayload.iat;
delete newUserPayload.exp;

View File

@ -60,12 +60,17 @@ import DocumentsNotaryController from "./api/notary/DocumentsNotaryController";
import DocumentsNotaryControllerCustomer from "./api/customer/DocumentsNotaryController";
import FilesNotaryController from "./api/notary/FilesNotaryController";
import FilesNotaryControllerCustomer from "./api/customer/FilesNotaryController";
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
/**
* @description This allow to declare all controllers used in the application
*/
export default {
start: () => {
// Initialize services
Container.get(SubscriptionBypass);
// Initialize controllers
Container.get(HomeController);
Container.get(UsersControllerSuperAdmin);
Container.get(FoldersControllerSuperAdmin);
@ -128,5 +133,5 @@ export default {
Container.get(FilesNotaryController);
Container.get(DocumentsNotaryControllerCustomer);
Container.get(FilesNotaryControllerCustomer);
},
}
};

View File

@ -0,0 +1,30 @@
import { Service } from "typedi";
import { BackendVariables } from "./variables/Variables";
@Service()
export class SubscriptionBypass {
constructor(private backendVariables: BackendVariables) {}
/**
* Check if subscription checks should be bypassed based on environment
*/
public shouldBypassSubscriptionChecks(): boolean {
return this.backendVariables.ENV === "dev" || this.backendVariables.ENV === "test";
}
/**
* Get the current environment for logging purposes
*/
public getCurrentEnvironment(): string {
return this.backendVariables.ENV;
}
/**
* Log subscription bypass information
*/
public logBypass(userId: string, officeId: string, context: string = ""): void {
if (this.shouldBypassSubscriptionChecks()) {
console.log(`[DEV/TEST] Bypassing subscription check for user ${userId} in office ${officeId} (ENV: ${this.getCurrentEnvironment()})${context ? ` - ${context}` : ""}`);
}
}
}

View File

@ -6,10 +6,15 @@ import SubscriptionsRepository from "@Repositories/SubscriptionsRepository";
import { Subscription } from "le-coffre-resources/dist/Admin";
import SeatsService from "../SeatsService/SeatsService";
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
@Service()
export default class SubscriptionsService extends BaseService {
constructor(private subscriptionsRepository: SubscriptionsRepository, private seatsService: SeatsService) {
constructor(
private subscriptionsRepository: SubscriptionsRepository,
private seatsService: SeatsService,
private subscriptionBypass: SubscriptionBypass
) {
super();
}
@ -63,6 +68,12 @@ export default class SubscriptionsService extends BaseService {
}
public async isUserSubscribed(userUid: string, officeUid: string): Promise<boolean> {
// Bypass subscription checks in development and test environments
if (this.subscriptionBypass.shouldBypassSubscriptionChecks()) {
this.subscriptionBypass.logBypass(userUid, officeUid, "isUserSubscribed method");
return true;
}
let isSubscribed = false;
const subscriptions = await this.get({ where: { office_uid: officeUid } });

View File

@ -0,0 +1,129 @@
import { Container } from "typedi";
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
import { BackendVariables } from "@Common/config/variables/Variables";
describe("SubscriptionBypass", () => {
let subscriptionBypass: SubscriptionBypass;
let backendVariables: any; // Use any to allow property mutation in tests
beforeEach(() => {
// Clear container and reset
Container.reset();
// Mock BackendVariables
backendVariables = {
ENV: "prod",
DATABASE_PORT: "5432",
DATABASE_HOST: "localhost",
DATABASE_USERNAME: "test",
DATABASE_PASSWORD: "test",
DATABASE_NAME: "test",
DATABASE_URL: "postgresql://test:test@localhost:5432/test",
API_ROOT_URL: "/api",
APP_HOST: "localhost",
APP_PORT: "3000",
APP_ROOT_URL: "/",
IDNOT_BASE_URL: "https://idnot.test",
IDNOT_API_BASE_URL: "https://api.idnot.test",
IDNOT_CONNEXION_URL: "https://connect.idnot.test",
IDNOT_CLIENT_ID: "test-client-id",
IDNOT_CLIENT_SECRET: "test-client-secret",
IDNOT_REDIRECT_URL: "https://redirect.test",
IDNOT_API_KEY: "test-api-key",
PINATA_API_KEY: "test-pinata-key",
PINATA_API_SECRET: "test-pinata-secret",
PINATA_GATEWAY: "https://gateway.test",
PINATA_GATEWAY_TOKEN: "test-gateway-token",
ACCESS_TOKEN_SECRET: "test-access-secret",
REFRESH_TOKEN_SECRET: "test-refresh-secret",
MAILCHIMP_API_KEY: "test-mailchimp-key",
SECURE_API_KEY: "test-secure-key",
SECURE_API_BASE_URL: "https://secure.test",
DOCAPOST_BASE_URL: "https://docapost.test",
DOCAPOST_ROOT: "/docapost",
DOCAPOST_VERSION: "v1",
DOCAPOST_DOCUMENT_PROCESS_ID: "test-process-id",
DOCAPOST_CONNECT_PROCESS_ID: "test-connect-process-id",
BACK_API_HOST: "localhost",
DOCAPOST_APP_ID: "test-app-id",
DOCAPOST_APP_PASSWORD: "test-app-password",
SMS_PROVIDER: "test-provider",
OVH_APP_KEY: "test-ovh-key",
OVH_APP_SECRET: "test-ovh-secret",
OVH_CONSUMER_KEY: "test-consumer-key",
OVH_SMS_SERVICE_NAME: "test-sms-service",
SMS_FACTOR_TOKEN: "test-sms-token",
SCW_ACCESS_KEY_ID: "test-access-key",
SCW_ACCESS_KEY_SECRET: "test-access-secret",
SCW_BUCKET_ENDPOINT: "https://bucket.test",
SCW_BUCKET_NAME: "test-bucket",
STRIPE_SECRET_KEY: "test-stripe-key",
STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID: "test-standard-price",
STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID: "test-standard-annual-price",
STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID: "test-unlimited-price",
STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID: "test-unlimited-annual-price",
IDNOT_PROD_BASE_URL: "https://prod.idnot.test",
MAILCHIMP_KEY: "test-mailchimp-key",
MAILCHIMP_LIST_ID: "test-list-id",
validate: jest.fn().mockResolvedValue(backendVariables)
};
Container.set(BackendVariables, backendVariables);
subscriptionBypass = Container.get(SubscriptionBypass);
});
describe("shouldBypassSubscriptionChecks", () => {
it("should return true for dev environment", () => {
backendVariables.ENV = "dev";
expect(subscriptionBypass.shouldBypassSubscriptionChecks()).toBe(true);
});
it("should return true for test environment", () => {
backendVariables.ENV = "test";
expect(subscriptionBypass.shouldBypassSubscriptionChecks()).toBe(true);
});
it("should return false for production environment", () => {
backendVariables.ENV = "prod";
expect(subscriptionBypass.shouldBypassSubscriptionChecks()).toBe(false);
});
it("should return false for staging environment", () => {
backendVariables.ENV = "staging";
expect(subscriptionBypass.shouldBypassSubscriptionChecks()).toBe(false);
});
});
describe("getCurrentEnvironment", () => {
it("should return the current environment", () => {
backendVariables.ENV = "dev";
expect(subscriptionBypass.getCurrentEnvironment()).toBe("dev");
});
});
describe("logBypass", () => {
it("should log bypass information when in dev/test environment", () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
backendVariables.ENV = "dev";
subscriptionBypass.logBypass("user123", "office456", "test context");
expect(consoleSpy).toHaveBeenCalledWith(
"[DEV/TEST] Bypassing subscription check for user user123 in office office456 (ENV: dev) - test context"
);
consoleSpy.mockRestore();
});
it("should not log bypass information when in production environment", () => {
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
backendVariables.ENV = "prod";
subscriptionBypass.logBypass("user123", "office456", "test context");
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
});