Compare commits
18 Commits
5cff87d927
...
5f50e13a50
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5f50e13a50 | ||
![]() |
bbc0c6ddcd | ||
![]() |
ce8fbf92fe | ||
![]() |
84ce2acf6f | ||
![]() |
06e69d6974 | ||
![]() |
e46739befe | ||
![]() |
86b81258b6 | ||
![]() |
5afa9d9a87 | ||
![]() |
c3c6185d9d | ||
![]() |
51c7c08c53 | ||
![]() |
4fdd163c90 | ||
![]() |
363c168479 | ||
![]() |
b2dfb98f2b | ||
![]() |
2435fb53b0 | ||
![]() |
497ff05ab1 | ||
![]() |
f3d9f3e6c8 | ||
![]() |
05191577eb | ||
![]() |
b048cfb1a0 |
84
SUBSCRIPTION_BYPASS.md
Normal file
84
SUBSCRIPTION_BYPASS.md
Normal 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
|
@ -63,8 +63,10 @@ export default class OfficeFoldersController extends ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId: string = req.body.user.userId;
|
const userId: string = req.body.user.userId;
|
||||||
|
const officeId: string = req.body.user.office_Id;
|
||||||
if (query.where?.stakeholders) delete query.where.stakeholders;
|
if (query.where?.stakeholders) delete query.where.stakeholders;
|
||||||
const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } } };
|
if (query.where?.office_uid) delete query.where.office_uid;
|
||||||
|
const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } }, office_uid: officeId };
|
||||||
query.where = officeFoldersWhereInput;
|
query.where = officeFoldersWhereInput;
|
||||||
|
|
||||||
//call service to get prisma entity
|
//call service to get prisma entity
|
||||||
|
@ -9,6 +9,7 @@ import User, { RulesGroup } from "le-coffre-resources/dist/Admin";
|
|||||||
import UsersService from "@Services/super-admin/UsersService/UsersService";
|
import UsersService from "@Services/super-admin/UsersService/UsersService";
|
||||||
import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts";
|
import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts";
|
||||||
import RulesGroupsService from "@Services/admin/RulesGroupsService/RulesGroupsService";
|
import RulesGroupsService from "@Services/admin/RulesGroupsService/RulesGroupsService";
|
||||||
|
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
@Service()
|
@Service()
|
||||||
@ -19,6 +20,7 @@ export default class UserController extends ApiController {
|
|||||||
private userService: UsersService,
|
private userService: UsersService,
|
||||||
private subscriptionsService: SubscriptionsService,
|
private subscriptionsService: SubscriptionsService,
|
||||||
private rulesGroupsService: RulesGroupsService,
|
private rulesGroupsService: RulesGroupsService,
|
||||||
|
private subscriptionBypass: SubscriptionBypass,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -100,21 +102,43 @@ export default class UserController extends ApiController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isSubscribed && (userHydrated.role?.name === "admin" || userHydrated.role?.name === "super-admin")) {
|
// In development/test environments, bypass admin role subscription requirements
|
||||||
|
if (this.subscriptionBypass.shouldBypassSubscriptionChecks()) {
|
||||||
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
|
if (!isSubscribed && (userHydrated.role?.name === "admin" || userHydrated.role?.name === "super-admin")) {
|
||||||
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
|
this.subscriptionBypass.logBypass(user.uid, userHydrated.office_membership?.uid!, "admin user login");
|
||||||
include: { rules: true },
|
|
||||||
});
|
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
|
||||||
|
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
|
||||||
|
include: { rules: true },
|
||||||
|
});
|
||||||
|
|
||||||
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
|
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
|
||||||
strategy: "excludeAll",
|
strategy: "excludeAll",
|
||||||
});
|
});
|
||||||
if (!manageSubscriptionRules[0]) return;
|
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) {
|
if (!isSubscribed) {
|
||||||
@ -162,20 +186,42 @@ export default class UserController extends ApiController {
|
|||||||
)) as IUserJwtPayload;
|
)) as IUserJwtPayload;
|
||||||
let isSubscribed = await this.subscriptionsService.isUserSubscribed(newUserPayload.userId, newUserPayload.office_Id);
|
let isSubscribed = await this.subscriptionsService.isUserSubscribed(newUserPayload.userId, newUserPayload.office_Id);
|
||||||
|
|
||||||
if (!isSubscribed && (newUserPayload.role === "admin" || newUserPayload.role === "super-admin")) {
|
// In development/test environments, bypass admin role subscription requirements
|
||||||
const manageSubscriptionRulesEntity = await this.rulesGroupsService.get({
|
if (this.subscriptionBypass.shouldBypassSubscriptionChecks()) {
|
||||||
where: { uid: "94343601-04c8-44ef-afb9-3047597528a9" },
|
if (!isSubscribed && (newUserPayload.role === "admin" || newUserPayload.role === "super-admin")) {
|
||||||
include: { rules: true },
|
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, {
|
const manageSubscriptionRules = RulesGroup.hydrateArray<RulesGroup>(manageSubscriptionRulesEntity, {
|
||||||
strategy: "excludeAll",
|
strategy: "excludeAll",
|
||||||
});
|
});
|
||||||
if (!manageSubscriptionRules[0]) return;
|
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.iat;
|
||||||
delete newUserPayload.exp;
|
delete newUserPayload.exp;
|
||||||
|
@ -60,8 +60,10 @@ export default class OfficeFoldersController extends ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId: string = req.body.user.userId;
|
const userId: string = req.body.user.userId;
|
||||||
|
const officeId: string = req.body.user.office_Id;
|
||||||
if (query.where?.stakeholders) delete query.where.stakeholders;
|
if (query.where?.stakeholders) delete query.where.stakeholders;
|
||||||
const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } } };
|
if (query.where?.office_uid) delete query.where.office_uid;
|
||||||
|
const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } }, office_uid: officeId };
|
||||||
query.where = officeFoldersWhereInput;
|
query.where = officeFoldersWhereInput;
|
||||||
|
|
||||||
//call service to get prisma entity
|
//call service to get prisma entity
|
||||||
@ -253,6 +255,7 @@ export default class OfficeFoldersController extends ApiController {
|
|||||||
protected async getOneByUid(req: Request, response: Response) {
|
protected async getOneByUid(req: Request, response: Response) {
|
||||||
try {
|
try {
|
||||||
const uid = req.params["uid"];
|
const uid = req.params["uid"];
|
||||||
|
const officeId: string = req.body.user.office_Id;
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
this.httpBadRequest(response, "No uid provided");
|
this.httpBadRequest(response, "No uid provided");
|
||||||
return;
|
return;
|
||||||
@ -270,6 +273,12 @@ export default class OfficeFoldersController extends ApiController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add office-level validation
|
||||||
|
if (officeFolderEntity.office_uid !== officeId) {
|
||||||
|
this.httpUnauthorized(response, "Not authorized to access this folder");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//Hydrate ressource with prisma entity
|
//Hydrate ressource with prisma entity
|
||||||
const officeFolder = OfficeFolder.hydrate<OfficeFolder>(officeFolderEntity);
|
const officeFolder = OfficeFolder.hydrate<OfficeFolder>(officeFolderEntity);
|
||||||
|
|
||||||
|
@ -61,8 +61,10 @@ export default class OfficeFoldersController extends ApiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userId: string = req.body.user.userId;
|
const userId: string = req.body.user.userId;
|
||||||
|
const officeId: string = req.body.user.office_Id;
|
||||||
if (query.where?.stakeholders) delete query.where.stakeholders;
|
if (query.where?.stakeholders) delete query.where.stakeholders;
|
||||||
const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } } };
|
if (query.where?.office_uid) delete query.where.office_uid;
|
||||||
|
const officeFoldersWhereInput: Prisma.OfficeFoldersWhereInput = { ...query.where, stakeholders: { some: { uid: userId } }, office_uid: officeId };
|
||||||
query.where = officeFoldersWhereInput;
|
query.where = officeFoldersWhereInput;
|
||||||
|
|
||||||
//call service to get prisma entity
|
//call service to get prisma entity
|
||||||
|
@ -60,12 +60,17 @@ import DocumentsNotaryController from "./api/notary/DocumentsNotaryController";
|
|||||||
import DocumentsNotaryControllerCustomer from "./api/customer/DocumentsNotaryController";
|
import DocumentsNotaryControllerCustomer from "./api/customer/DocumentsNotaryController";
|
||||||
import FilesNotaryController from "./api/notary/FilesNotaryController";
|
import FilesNotaryController from "./api/notary/FilesNotaryController";
|
||||||
import FilesNotaryControllerCustomer from "./api/customer/FilesNotaryController";
|
import FilesNotaryControllerCustomer from "./api/customer/FilesNotaryController";
|
||||||
|
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description This allow to declare all controllers used in the application
|
* @description This allow to declare all controllers used in the application
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
start: () => {
|
start: () => {
|
||||||
|
// Initialize services
|
||||||
|
Container.get(SubscriptionBypass);
|
||||||
|
|
||||||
|
// Initialize controllers
|
||||||
Container.get(HomeController);
|
Container.get(HomeController);
|
||||||
Container.get(UsersControllerSuperAdmin);
|
Container.get(UsersControllerSuperAdmin);
|
||||||
Container.get(FoldersControllerSuperAdmin);
|
Container.get(FoldersControllerSuperAdmin);
|
||||||
@ -128,5 +133,5 @@ export default {
|
|||||||
Container.get(FilesNotaryController);
|
Container.get(FilesNotaryController);
|
||||||
Container.get(DocumentsNotaryControllerCustomer);
|
Container.get(DocumentsNotaryControllerCustomer);
|
||||||
Container.get(FilesNotaryControllerCustomer);
|
Container.get(FilesNotaryControllerCustomer);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
30
src/common/config/SubscriptionBypass.ts
Normal file
30
src/common/config/SubscriptionBypass.ts
Normal 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}` : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -100,9 +100,9 @@ export default class UsersRepository extends BaseRepository {
|
|||||||
update: {
|
update: {
|
||||||
first_name: user.contact?.first_name,
|
first_name: user.contact?.first_name,
|
||||||
last_name: user.contact?.last_name,
|
last_name: user.contact?.last_name,
|
||||||
email: user.contact?.email,
|
email: user.contact?.email === null ? undefined : user.contact?.email,
|
||||||
phone_number: user.contact?.phone_number,
|
phone_number: user.contact?.phone_number === null ? undefined : user.contact?.phone_number,
|
||||||
cell_phone_number: user.contact?.cell_phone_number,
|
cell_phone_number: user.contact?.cell_phone_number === null ? undefined : user.contact?.cell_phone_number,
|
||||||
civility: ECivility[user.contact?.civility as keyof typeof ECivility],
|
civility: ECivility[user.contact?.civility as keyof typeof ECivility],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -6,10 +6,15 @@ import SubscriptionsRepository from "@Repositories/SubscriptionsRepository";
|
|||||||
import { Subscription } from "le-coffre-resources/dist/Admin";
|
import { Subscription } from "le-coffre-resources/dist/Admin";
|
||||||
import SeatsService from "../SeatsService/SeatsService";
|
import SeatsService from "../SeatsService/SeatsService";
|
||||||
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
|
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
|
||||||
|
import { SubscriptionBypass } from "@Common/config/SubscriptionBypass";
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class SubscriptionsService extends BaseService {
|
export default class SubscriptionsService extends BaseService {
|
||||||
constructor(private subscriptionsRepository: SubscriptionsRepository, private seatsService: SeatsService) {
|
constructor(
|
||||||
|
private subscriptionsRepository: SubscriptionsRepository,
|
||||||
|
private seatsService: SeatsService,
|
||||||
|
private subscriptionBypass: SubscriptionBypass
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +68,12 @@ export default class SubscriptionsService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async isUserSubscribed(userUid: string, officeUid: string): Promise<boolean> {
|
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;
|
let isSubscribed = false;
|
||||||
|
|
||||||
const subscriptions = await this.get({ where: { office_uid: officeUid } });
|
const subscriptions = await this.get({ where: { office_uid: officeUid } });
|
||||||
|
@ -68,8 +68,8 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public async updateUsers() {
|
public async updateUsers() {
|
||||||
const cronJob = new CronJob("0 0 * * *", async () => {
|
const cronJob = new CronJob("*/5 * * * *", async () => {
|
||||||
// Once a day at midnight
|
// Every 5 minutes
|
||||||
try {
|
try {
|
||||||
await this.idNotService.updateOffices();
|
await this.idNotService.updateOffices();
|
||||||
await this.idNotService.updateUsers();
|
await this.idNotService.updateUsers();
|
||||||
|
@ -50,6 +50,7 @@ interface IRattachementData {
|
|||||||
numeroTelephone: string;
|
numeroTelephone: string;
|
||||||
statutDuRattachement: boolean;
|
statutDuRattachement: boolean;
|
||||||
mailRattachement: string;
|
mailRattachement: string;
|
||||||
|
deleted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IOfficeData {
|
interface IOfficeData {
|
||||||
@ -225,45 +226,63 @@ export default class IdNotService extends BaseService {
|
|||||||
key: this.variables.IDNOT_API_KEY,
|
key: this.variables.IDNOT_API_KEY,
|
||||||
});
|
});
|
||||||
|
|
||||||
let userData: IRattachementData;
|
let userRawData: Response;
|
||||||
try {
|
try {
|
||||||
userData = (await (
|
userRawData = await fetch(
|
||||||
await fetch(
|
|
||||||
`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${user.idNot}_${user.office_membership!.idNot}?` +
|
`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/rattachements/${user.idNot}_${user.office_membership!.idNot}?` +
|
||||||
searchParams,
|
searchParams,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
).json()) as IRattachementData;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching user data", error);
|
console.error(`Error fetching user data for ${user.uid}: ${error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userRawData.status === 404) {
|
||||||
|
console.error(`User ${user.uid} not found in idnot`);
|
||||||
|
return;
|
||||||
|
} else if (userRawData.status !== 200) {
|
||||||
|
console.error(`Error fetching user data for ${user.uid}: ${userRawData.status} - ${userRawData.statusText}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userData.statutDuRattachement) {
|
let userData = (await userRawData.json()) as IRattachementData;
|
||||||
|
|
||||||
|
if (userData.deleted) {
|
||||||
let rattachements: any;
|
let rattachements: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rattachements = (await (
|
const res = await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/personnes/${user.idNot}/rattachements?` + searchParams, {
|
||||||
await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/personnes/${user.idNot}/rattachements?` + searchParams, {
|
method: "GET",
|
||||||
method: "GET",
|
});
|
||||||
})
|
if (res.status === 404) {
|
||||||
).json()) as any;
|
console.error(`User ${user.uid} not found in idnot`);
|
||||||
|
return;
|
||||||
|
} else if (res.status !== 200) {
|
||||||
|
console.error(`Error fetching rattachements for ${user.uid}: ${rattachements.status} - ${rattachements.statusText}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rattachements = await res.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching rattachements", error);
|
console.error("Error fetching rattachements", error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rattachements.totalResultCount === 0) {
|
if (rattachements && rattachements.totalResultCount === 0 && rattachements.result.length === 0) {
|
||||||
|
console.warn("User has no valid rattachements", user.uid);
|
||||||
await this.userService.updateCheckedAt(user.uid!);
|
await this.userService.updateCheckedAt(user.uid!);
|
||||||
//await this.userService.delete(user.uid!);
|
|
||||||
return;
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(`Found ${rattachements.totalResultCount} rattachements for ${user.uid}`);
|
||||||
}
|
}
|
||||||
const rattachementsResults = rattachements.result as IRattachementData[];
|
const rattachementsResults = rattachements.result as IRattachementData[];
|
||||||
if (!rattachementsResults) return;
|
if (!rattachementsResults) return;
|
||||||
rattachementsResults.forEach(async (rattachement) => {
|
|
||||||
if (rattachement.statutDuRattachement) {
|
for (const rattachement of rattachementsResults) {
|
||||||
|
if (rattachement.statutDuRattachement && !rattachement.deleted) {
|
||||||
let officeData: IOfficeData;
|
let officeData: IOfficeData;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -274,20 +293,22 @@ export default class IdNotService extends BaseService {
|
|||||||
).json()) as IOfficeData;
|
).json()) as IOfficeData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching office data", error);
|
console.error("Error fetching office data", error);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (officeData.typeEntite.name === "office") {
|
if (officeData.typeEntite.name === "office") {
|
||||||
userData = rattachement;
|
userData = rattachement;
|
||||||
|
userData.entite = officeData;
|
||||||
|
break; // Found the first valid office, no need to continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updates = 0;
|
let update = false;
|
||||||
|
|
||||||
if (user.office_membership!.idNot !== userData.entite.ou) {
|
if (user.office_membership!.idNot !== userData.entite.ou) {
|
||||||
updates++;
|
update = true;
|
||||||
let officeData = (await this.officeService.get({ where: { idNot: userData.entite.ou } }))[0];
|
let officeData = (await this.officeService.get({ where: { idNot: userData.entite.ou } }))[0];
|
||||||
if (!officeData) {
|
if (!officeData) {
|
||||||
let officeLocationData: IOfficeLocation;
|
let officeLocationData: IOfficeLocation;
|
||||||
@ -321,17 +342,25 @@ export default class IdNotService extends BaseService {
|
|||||||
officeData = await this.officeService.create(office);
|
officeData = await this.officeService.create(office);
|
||||||
}
|
}
|
||||||
user.office_membership = officeData;
|
user.office_membership = officeData;
|
||||||
|
console.log("New office_membership found", JSON.stringify(user.office_membership));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.contact!.email !== userData.mailRattachement) {
|
if (userData.mailRattachement && (user.contact!.email === null || user.contact!.email === undefined || user.contact!.email !== userData.mailRattachement)) {
|
||||||
updates++;
|
update = true;
|
||||||
user.contact!.email = userData.mailRattachement;
|
user.contact!.email = userData.mailRattachement;
|
||||||
|
console.log("New email found", JSON.stringify(user.contact!.email));
|
||||||
}
|
}
|
||||||
if (user.contact!.cell_phone_number !== userData.numeroMobile) {
|
if (userData.numeroMobile && (user.contact!.cell_phone_number === null || user.contact!.cell_phone_number === undefined || user.contact!.cell_phone_number !== userData.numeroMobile)) {
|
||||||
updates++;
|
update = true;
|
||||||
user.contact!.cell_phone_number = userData.numeroMobile;
|
user.contact!.cell_phone_number = userData.numeroMobile;
|
||||||
|
console.log("New cell phone number found", JSON.stringify(user.contact!.cell_phone_number));
|
||||||
|
}
|
||||||
|
if (update) {
|
||||||
|
console.log("Found updates for user", user.uid!);
|
||||||
|
// Filter out null values before updating to prevent Prisma errors
|
||||||
|
const convertedUser = this.convertNullToUndefined(user);
|
||||||
|
await this.userService.update(user.uid!, convertedUser);
|
||||||
}
|
}
|
||||||
if (updates != 0) await this.userService.update(user.uid!, user);
|
|
||||||
await this.userService.updateCheckedAt(user.uid!);
|
await this.userService.updateCheckedAt(user.uid!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,19 +388,20 @@ export default class IdNotService extends BaseService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const officeData = (await officeRawData.json()) as IOfficeData;
|
const officeData = (await officeRawData.json()) as IOfficeData;
|
||||||
console.log("office", office);
|
|
||||||
|
|
||||||
console.log("officeData", officeData);
|
|
||||||
|
|
||||||
let updates = 0;
|
let updates = 0;
|
||||||
//093051 = demo
|
//093051 = demo
|
||||||
if (office.name !== officeData.denominationSociale && office.name !== officeData.codeCrpcen && office.crpcen !== "029178" && office.crpcen !== "035010" && office.crpcen !== "093051") {
|
if (office.name !== officeData.denominationSociale && office.name !== officeData.codeCrpcen && office.crpcen !== "029178" && office.crpcen !== "035010" && office.crpcen !== "093051") {
|
||||||
|
console.log(`Updating office name: ${office.uid} - ${office.name} - ${office.crpcen}`);
|
||||||
updates++;
|
updates++;
|
||||||
office.name = officeData.denominationSociale ?? officeData.codeCrpcen;
|
office.name = officeData.denominationSociale ?? officeData.codeCrpcen;
|
||||||
|
console.log(`New name: ${office.name}`);
|
||||||
}
|
}
|
||||||
if (office.office_status !== this.getOfficeStatus(officeData.statutEntite.name)) {
|
if (office.office_status !== this.getOfficeStatus(officeData.statutEntite.name)) {
|
||||||
|
console.log(`Updating office status: ${office.uid} - ${office.name} - ${office.crpcen}`);
|
||||||
updates++;
|
updates++;
|
||||||
office.office_status = this.getOfficeStatus(officeData.statutEntite.name);
|
office.office_status = this.getOfficeStatus(officeData.statutEntite.name);
|
||||||
|
console.log(`New status: ${office.office_status}`);
|
||||||
}
|
}
|
||||||
if (updates != 0) await this.officeService.update(office.uid!, office);
|
if (updates != 0) await this.officeService.update(office.uid!, office);
|
||||||
await this.officeService.updateCheckedAt(office.uid!);
|
await this.officeService.updateCheckedAt(office.uid!);
|
||||||
@ -541,16 +571,36 @@ export default class IdNotService extends BaseService {
|
|||||||
public async updateUsers() {
|
public async updateUsers() {
|
||||||
const usersReq = await this.userService.getUsersToBeChecked();
|
const usersReq = await this.userService.getUsersToBeChecked();
|
||||||
const users = User.hydrateArray<User>(usersReq);
|
const users = User.hydrateArray<User>(usersReq);
|
||||||
users.forEach(async (user) => {
|
for (const user of users) {
|
||||||
await this.updateUser(user.uid!);
|
await this.updateUser(user.uid!);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateOffices() {
|
public async updateOffices() {
|
||||||
const officesReq = await this.officeService.getOfficesToBeChecked();
|
const officesReq = await this.officeService.getOfficesToBeChecked();
|
||||||
const offices = Office.hydrateArray<Office>(officesReq);
|
const offices = Office.hydrateArray<Office>(officesReq);
|
||||||
offices.forEach(async (office) => {
|
console.log(`Updating ${offices.length} offices`);
|
||||||
|
for (const office of offices) {
|
||||||
await this.updateOffice(office.uid!);
|
await this.updateOffice(office.uid!);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to convert null values to undefined
|
||||||
|
* This prevents Prisma from throwing errors about null values
|
||||||
|
*/
|
||||||
|
private convertNullToUndefined<T extends Record<string, any>>(obj: T): T {
|
||||||
|
const converted = { ...obj };
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(converted)) {
|
||||||
|
if (value === null) {
|
||||||
|
console.log(`Converting null to undefined for key: ${key}`);
|
||||||
|
(converted as any)[key] = undefined;
|
||||||
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
(converted as any)[key] = this.convertNullToUndefined(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
129
src/test/subscription-bypass.test.ts
Normal file
129
src/test/subscription-bypass.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user