Skip to content

Commit 9a8c321

Browse files
committed
feat: implement framework-agnostic services for authentication, storage, themes, encryption, file handling, global settings, Google Pay, and translation
1 parent e7fc98d commit 9a8c321

File tree

9 files changed

+494
-0
lines changed

9 files changed

+494
-0
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { AuthService, AuthServiceConfig } from './auth-services';
2+
3+
describe('AuthService', () => {
4+
const config: AuthServiceConfig = {
5+
apiBaseUrl: 'https://mixcore.net',
6+
encryptAES: (data) => data, // mock encryption
7+
updateAuthData: jest.fn(),
8+
fillAuthData: jest.fn(async () => ({ info: { userRoles: [{ description: 'Admin', isActived: true }] } })),
9+
initAllSettings: jest.fn(async () => {}),
10+
getApiResult: jest.fn(async (req) => ({ isSucceed: true, data: {} })),
11+
getRestApiResult: jest.fn(async (req) => ({ isSucceed: true, data: {} })),
12+
localStorage: { removeItem: jest.fn() } as any,
13+
};
14+
const service = new AuthService(config);
15+
16+
it('should call saveRegistration', async () => {
17+
await service.saveRegistration({});
18+
expect(config.getApiResult).toHaveBeenCalled();
19+
});
20+
21+
it('should call forgotPassword', async () => {
22+
await service.forgotPassword({});
23+
expect(config.getRestApiResult).toHaveBeenCalled();
24+
});
25+
26+
it('should call login and updateAuthData', async () => {
27+
await service.login({ username: 'user', password: 'pass' });
28+
expect(config.updateAuthData).toHaveBeenCalled();
29+
});
30+
31+
it('should call logOut and removeItem', async () => {
32+
await service.logOut();
33+
expect(config.localStorage?.removeItem).toHaveBeenCalledWith('authorizationData');
34+
});
35+
36+
it('should call isInRole', async () => {
37+
await service.fillAuthData();
38+
expect(service.isInRole('Admin')).toBe(true);
39+
});
40+
});

packages/apis/src/auth-services.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* AuthService
3+
* Framework-agnostic, TypeScript-native authentication API client for Mixcore
4+
*
5+
* @remarks
6+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
7+
* Configuration is injected via constructor.
8+
*/
9+
export interface AuthServiceConfig {
10+
apiBaseUrl: string;
11+
apiKey?: string;
12+
encryptAES: (data: string) => string;
13+
updateAuthData: (data: any) => void;
14+
fillAuthData: () => Promise<any>;
15+
initAllSettings: () => Promise<void>;
16+
getApiResult: (req: any) => Promise<any>;
17+
getRestApiResult: (req: any, ...args: any[]) => Promise<any>;
18+
localStorage?: Storage;
19+
}
20+
21+
export class AuthService {
22+
private config: AuthServiceConfig;
23+
public authentication: any = null;
24+
25+
constructor(config: AuthServiceConfig) {
26+
this.config = config;
27+
}
28+
29+
async saveRegistration(registration: any): Promise<any> {
30+
return this.config.getApiResult({
31+
method: 'POST',
32+
url: '/account/register',
33+
data: registration,
34+
});
35+
}
36+
37+
async forgotPassword(data: any): Promise<any> {
38+
return this.config.getRestApiResult({
39+
method: 'POST',
40+
url: '/account/forgot-password',
41+
data: JSON.stringify(data),
42+
});
43+
}
44+
45+
async resetPassword(data: any): Promise<any> {
46+
return this.config.getRestApiResult({
47+
method: 'POST',
48+
url: '/account/reset-password',
49+
data: JSON.stringify(data),
50+
});
51+
}
52+
53+
async login(loginData: { username: string; password: string; rememberMe?: boolean }): Promise<any> {
54+
const data = {
55+
UserName: loginData.username,
56+
Password: loginData.password,
57+
RememberMe: loginData.rememberMe,
58+
Email: '',
59+
ReturnUrl: '',
60+
};
61+
const message = this.config.encryptAES(JSON.stringify(data));
62+
const req = {
63+
method: 'POST',
64+
url: '/account/login',
65+
data: JSON.stringify({ message }),
66+
};
67+
const resp = await this.config.getRestApiResult(req, true);
68+
if (resp.isSucceed) {
69+
this.config.updateAuthData(resp.data);
70+
await this.config.initAllSettings();
71+
}
72+
return resp;
73+
}
74+
75+
async externalLogin(loginData: any, provider: string): Promise<any> {
76+
const data = {
77+
provider,
78+
username: loginData.username,
79+
email: loginData.email,
80+
externalAccessToken: loginData.accessToken,
81+
};
82+
const message = this.config.encryptAES(JSON.stringify(data));
83+
const req = {
84+
method: 'POST',
85+
url: '/account/external-login',
86+
data: JSON.stringify({ message }),
87+
};
88+
const resp = await this.config.getRestApiResult(req, true);
89+
if (resp.isSucceed) {
90+
this.config.updateAuthData(resp.data);
91+
await this.config.initAllSettings();
92+
}
93+
return resp;
94+
}
95+
96+
async logOut(): Promise<void> {
97+
if (this.config.localStorage) {
98+
this.config.localStorage.removeItem('authorizationData');
99+
}
100+
// Optionally, call /account/logout endpoint if needed
101+
}
102+
103+
async fillAuthData(): Promise<void> {
104+
this.authentication = await this.config.fillAuthData();
105+
}
106+
107+
async refreshToken(id: string, accessToken: string): Promise<any> {
108+
if (!id) return this.logOut();
109+
const req = {
110+
method: 'POST',
111+
url: '/account/refresh-token',
112+
data: JSON.stringify({ refreshToken: id, accessToken }),
113+
};
114+
const resp = await this.config.getApiResult(req);
115+
if (resp.isSucceed) {
116+
return this.config.updateAuthData(resp.data);
117+
} else {
118+
return this.logOut();
119+
}
120+
}
121+
122+
isInRole(roleName: string): boolean {
123+
if (!this.authentication || !this.authentication.info) return false;
124+
const roles = this.authentication.info.userRoles || [];
125+
return roles.some((m: any) => m.description === roleName && m.isActived);
126+
}
127+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* StoreService
3+
* Framework-agnostic, TypeScript-native store API client for Mixcore
4+
*
5+
* @remarks
6+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
7+
* Configuration is injected via constructor.
8+
*/
9+
export interface StoreServiceConfig {
10+
apiBaseUrl: string;
11+
apiKey?: string;
12+
getRestApiResult: (req: any) => Promise<any>;
13+
}
14+
15+
export class StoreService {
16+
private config: StoreServiceConfig;
17+
private serviceBase = 'https://store.mixcore.org';
18+
19+
constructor(config: StoreServiceConfig) {
20+
this.config = config;
21+
}
22+
23+
async getThemes(objData?: Record<string, any>): Promise<any> {
24+
const params = objData ? this.parseQuery(objData) : '';
25+
let url = '/rest/en-us/post/client';
26+
if (params) url += '?' + params;
27+
const req = { serviceBase: this.serviceBase, method: 'GET', url };
28+
return this.config.getRestApiResult(req);
29+
}
30+
31+
async getCategories(objData?: Record<string, any>): Promise<any> {
32+
const params = objData ? this.parseQuery(objData) : '';
33+
let url = '/rest/en-us/mix-database-data/client';
34+
if (params) url += '?' + params;
35+
const req = { serviceBase: this.serviceBase, method: 'GET', url };
36+
return this.config.getRestApiResult(req);
37+
}
38+
39+
private parseQuery(obj: Record<string, any>): string {
40+
return Object.entries(obj)
41+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
42+
.join('&');
43+
}
44+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* ThemeService
3+
* Framework-agnostic, TypeScript-native theme API client for Mixcore
4+
*
5+
* @remarks
6+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
7+
* Configuration is injected via constructor.
8+
*/
9+
export interface ThemeServiceConfig {
10+
apiBaseUrl: string;
11+
apiKey?: string;
12+
getRestApiResult: (req: any) => Promise<any>;
13+
}
14+
15+
export class ThemeService {
16+
private config: ThemeServiceConfig;
17+
private prefixUrl = 'theme/portal';
18+
19+
constructor(config: ThemeServiceConfig) {
20+
this.config = config;
21+
}
22+
23+
async syncTemplates(id: string): Promise<any> {
24+
const url = `${this.prefixUrl}/sync/${id}`;
25+
const req = { method: 'GET', url };
26+
return this.config.getRestApiResult(req);
27+
}
28+
29+
async install(objData: any): Promise<any> {
30+
const url = `${this.prefixUrl}/install`;
31+
const req = { method: 'POST', url, data: JSON.stringify(objData) };
32+
return this.config.getRestApiResult(req);
33+
}
34+
35+
async export(id: string, objData: any): Promise<any> {
36+
const url = `${this.prefixUrl}/export/${id}`;
37+
const req = { method: 'POST', url, data: JSON.stringify(objData) };
38+
return this.config.getRestApiResult(req);
39+
}
40+
41+
async getExportData(id: string): Promise<any> {
42+
const url = `${this.prefixUrl}/export/${id}`;
43+
const req = { method: 'GET', url };
44+
return this.config.getRestApiResult(req);
45+
}
46+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* CryptoService
3+
* Framework-agnostic AES encryption/decryption utility for Mixcore SDK
4+
*
5+
* @remarks
6+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
7+
* Uses CryptoJS (must be provided by consumer).
8+
*/
9+
export interface CryptoServiceConfig {
10+
apiEncryptKey: string;
11+
CryptoJS: any;
12+
}
13+
14+
export class CryptoService {
15+
private config: CryptoServiceConfig;
16+
private size = 256;
17+
18+
constructor(config: CryptoServiceConfig) {
19+
this.config = config;
20+
}
21+
22+
encryptAES(message: string, iCompleteEncodedKey?: string): string {
23+
const { key, iv } = this.parseKeys(iCompleteEncodedKey || this.config.apiEncryptKey);
24+
return this.encryptMessage(message, key, iv);
25+
}
26+
27+
decryptAES(ciphertext: string, iCompleteEncodedKey?: string): string {
28+
const { key, iv } = this.parseKeys(iCompleteEncodedKey || this.config.apiEncryptKey);
29+
return this.decryptMessage(ciphertext, key, iv);
30+
}
31+
32+
private encryptMessage(message: string, key: any, iv: any): string {
33+
const { CryptoJS } = this.config;
34+
const options = {
35+
iv,
36+
keySize: this.size / 8,
37+
mode: CryptoJS.mode.CBC,
38+
padding: CryptoJS.pad.Pkcs7,
39+
};
40+
const encrypted = CryptoJS.AES.encrypt(message, key, options);
41+
return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
42+
}
43+
44+
private decryptMessage(ciphertext: string, key: any, iv: any): string {
45+
const { CryptoJS } = this.config;
46+
const cipherParams = CryptoJS.lib.CipherParams.create({
47+
ciphertext: CryptoJS.enc.Base64.parse(ciphertext),
48+
});
49+
const options = {
50+
iv,
51+
keySize: this.size / 8,
52+
mode: CryptoJS.mode.CBC,
53+
padding: CryptoJS.pad.Pkcs7,
54+
};
55+
const decrypted = CryptoJS.AES.decrypt(cipherParams, key, options);
56+
return decrypted.toString(CryptoJS.enc.Utf8);
57+
}
58+
59+
private parseKeys(iCompleteEncodedKey: string) {
60+
const { CryptoJS } = this.config;
61+
const keyStrings = CryptoJS.enc.Utf8.stringify(
62+
CryptoJS.enc.Base64.parse(iCompleteEncodedKey)
63+
).split(",");
64+
return {
65+
iv: CryptoJS.enc.Base64.parse(keyStrings[0]),
66+
key: CryptoJS.enc.Base64.parse(keyStrings[1]),
67+
};
68+
}
69+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* FileService
3+
* Framework-agnostic file utility for Mixcore SDK
4+
*
5+
* @remarks
6+
* Refactored from legacy AngularJS service. All SPA dependencies removed.
7+
* Extends base service pattern.
8+
*/
9+
export interface FileServiceConfig {
10+
apiBaseUrl: string;
11+
apiKey?: string;
12+
}
13+
14+
export class FileService {
15+
private config: FileServiceConfig;
16+
private prefixUrl = 'file';
17+
18+
constructor(config: FileServiceConfig) {
19+
this.config = config;
20+
}
21+
22+
// Add file-related methods as needed
23+
}

0 commit comments

Comments
 (0)