@siran/auth-core 0.1.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +21 -0
- package/CHANGELOG.md +139 -0
- package/COMPOSITE_AUTH_SERVICE.md +104 -0
- package/package.json +1 -1
- package/src/application/policies/account-already-exists.policy.ts +23 -0
- package/src/application/policies/magic-link-token.policy.ts +23 -0
- package/src/application/policies/oauth-code.policy.ts +23 -0
- package/src/application/policies/oauth-provider.policy.ts +29 -0
- package/src/application/policies/otp-code-format.policy.ts +23 -0
- package/src/application/policies/otp-code.policy.ts +23 -0
- package/src/application/policies/otp-identifier.policy.ts +23 -0
- package/src/application/policies/password-identifier.policy.ts +23 -0
- package/src/application/policies/password-min-length.policy.ts +29 -0
- package/src/application/policies/password-present.policy.ts +23 -0
- package/src/application/policies/password-strength.policy.ts +27 -0
- package/src/application/ports/auth-preference-service.port.ts +14 -0
- package/src/application/ports/auth-service.port.ts +58 -11
- package/src/application/ports/pre-register-policy.port.ts +7 -0
- package/src/application/ports/user-existence.port.ts +3 -0
- package/src/application/services/composite-auth.service.ts +96 -0
- package/src/application/use-cases/authenticate-user.ts +17 -4
- package/src/application/use-cases/register-user.ts +28 -0
- package/src/auth-engine.factory.ts +53 -0
- package/src/auth-engine.ts +25 -0
- package/src/domain/user-account.ts +3 -9
- package/src/index.ts +16 -2
- package/tests/application/policies/magic-link-token.policy.test.ts +74 -0
- package/tests/application/policies/oauth-code.policy.test.ts +79 -0
- package/tests/application/policies/oauth-provider.policy.test.ts +76 -0
- package/tests/application/policies/otp-code-format.policy.test.ts +90 -0
- package/tests/application/policies/otp-code.policy.test.ts +55 -0
- package/tests/application/policies/otp-identifier.policy.test.ts +55 -0
- package/tests/application/policies/password-identifier.policy.test.ts +55 -0
- package/tests/application/policies/password-min-length.policy.test.ts +66 -0
- package/tests/application/policies/password-present.policy.test.ts +55 -0
- package/tests/application/policies/password-strength.policy.test.ts +66 -0
- package/tests/application/ports/auth-preferences-service.port.mock.ts +14 -0
- package/tests/application/ports/auth-service.port.mock.ts +19 -0
- package/tests/application/ports/pre-register-policy.port.mock.ts +16 -0
- package/tests/application/services/composite-auth.mock.ts +18 -0
- package/tests/application/services/composite-auth.test.ts +162 -0
- package/tests/application/use-cases/authenticate-user.test.ts +61 -14
- package/tests/application/use-cases/register-user.test.ts +89 -0
- package/tests/domain/user-account.data.ts +5 -0
- package/tsconfig.lib.json +4 -1
- package/tsconfig.spec.json +13 -1
- package/vite.config.mts +4 -1
- package/src/domain/session.ts +0 -16
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthMethod,
|
|
3
|
+
AuthResult,
|
|
4
|
+
AuthServicePort,
|
|
5
|
+
} from '@application/ports/auth-service.port.js';
|
|
6
|
+
|
|
7
|
+
export type AuthMethodConfig = Partial<
|
|
8
|
+
Record<AuthMethod['type'], AuthServicePort>
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
export class CompositeAuthService implements AuthServicePort {
|
|
12
|
+
private readonly methodConfig: AuthMethodConfig;
|
|
13
|
+
private readonly adapters: AuthServicePort[];
|
|
14
|
+
|
|
15
|
+
constructor(config: AuthMethodConfig) {
|
|
16
|
+
this.methodConfig = config;
|
|
17
|
+
// Extract all adapters from config for operations like logout
|
|
18
|
+
this.adapters = Object.values(config).filter(
|
|
19
|
+
(adapter): adapter is AuthServicePort => adapter !== undefined
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async authenticate(method: AuthMethod): Promise<AuthResult> {
|
|
24
|
+
const adapter = this.getAdapterForMethod(method);
|
|
25
|
+
|
|
26
|
+
if (!adapter) {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
error: 'ADAPTER_UNAVAILABLE',
|
|
30
|
+
violatedPolicies: [],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return await adapter.authenticate(method);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Authentication error in adapter:', error);
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: 'NETWORK_ERROR',
|
|
41
|
+
violatedPolicies: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async register(method: AuthMethod): Promise<AuthResult> {
|
|
47
|
+
const adapter = this.getAdapterForMethod(method);
|
|
48
|
+
|
|
49
|
+
if (!adapter) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
error: 'ADAPTER_UNAVAILABLE',
|
|
53
|
+
violatedPolicies: [],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return await adapter.register(method);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('Registration error in adapter:', error);
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
error: 'NETWORK_ERROR',
|
|
64
|
+
violatedPolicies: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async logout(): Promise<void> {
|
|
70
|
+
// Try to logout from all adapters
|
|
71
|
+
const logoutPromises = this.adapters.map(async (adapter) => {
|
|
72
|
+
try {
|
|
73
|
+
await adapter.logout();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('Logout error in adapter:', error);
|
|
76
|
+
// Continue with other adapters even if one fails
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await Promise.allSettled(logoutPromises);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get adapter for specific auth method
|
|
85
|
+
*/
|
|
86
|
+
private getAdapterForMethod(method: AuthMethod): AuthServicePort | undefined {
|
|
87
|
+
return this.methodConfig[method.type];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get method configuration (read-only)
|
|
92
|
+
*/
|
|
93
|
+
getMethodConfig(): AuthMethodConfig {
|
|
94
|
+
return { ...this.methodConfig };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import { AuthPreferencesServicePort } from "@application/ports/auth-preference-service.port.js";
|
|
2
|
+
import type { AuthMethod, AuthResult } from "@application/ports/auth-service.port.js";
|
|
1
3
|
import { AuthServicePort } from "@application/ports/auth-service.port.js";
|
|
2
|
-
import type { AuthMethod } from "@application/ports/auth-service.port.js";
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
export const authenticateUser =
|
|
6
|
-
(authService: AuthServicePort) =>
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
(authService: AuthServicePort, authPreferencesService: AuthPreferencesServicePort) =>
|
|
8
|
+
async (method: AuthMethod): Promise<AuthResult> => {
|
|
9
|
+
const result = await authService.authenticate(method);
|
|
10
|
+
if (!result.ok) {
|
|
11
|
+
return { ok: false, error: result.error, violatedPolicies: [] };
|
|
12
|
+
}
|
|
13
|
+
if (result.user.status === 'disabled') {
|
|
14
|
+
return { ok: false, error: 'ACCOUNT_DISABLED', violatedPolicies: [] };
|
|
15
|
+
}
|
|
16
|
+
const preferences = await authPreferencesService.getAuthPreferences();
|
|
17
|
+
if (!result.user.isVerified && !preferences.allowLoginWithNotVerifiedAccount) {
|
|
18
|
+
return { ok: false, error: 'ACCOUNT_NOT_VERIFIED', violatedPolicies: [] };
|
|
19
|
+
}
|
|
20
|
+
return { ok: true, user: result.user };
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AuthMethod, AuthResult, AuthServicePort } from "@application/ports/auth-service.port.js";
|
|
2
|
+
import { PreRegisterPolicy } from "@application/ports/pre-register-policy.port.js";
|
|
3
|
+
|
|
4
|
+
export const registerUser =
|
|
5
|
+
(authService: AuthServicePort, policies: PreRegisterPolicy[]) =>
|
|
6
|
+
async (method: AuthMethod): Promise<AuthResult> => {
|
|
7
|
+
const activePolicies = await Promise.all(policies
|
|
8
|
+
.map(async policy => ({
|
|
9
|
+
isActive: await policy.isActive(method),
|
|
10
|
+
value: policy,
|
|
11
|
+
}))
|
|
12
|
+
);
|
|
13
|
+
const results = await Promise.all(activePolicies
|
|
14
|
+
.filter(policy => policy.isActive)
|
|
15
|
+
.map(async policy => ({
|
|
16
|
+
code: policy.value.code,
|
|
17
|
+
ok: await policy.value.check(method),
|
|
18
|
+
})));
|
|
19
|
+
const violdatedPolicies = results.filter(result => !result.ok);
|
|
20
|
+
if (violdatedPolicies.length > 0) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
error: 'PRE_REGISTER_POLICY_VIOLATED',
|
|
24
|
+
violatedPolicies: violdatedPolicies.map(result => result.code)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return authService.register(method);
|
|
28
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { AccountAlreadyExistsPolicy } from '@application/policies/account-already-exists.policy.js';
|
|
2
|
+
import { OtpCodePolicy } from '@application/policies/otp-code.policy.js';
|
|
3
|
+
import { OtpIdentifierPolicy } from '@application/policies/otp-identifier.policy.js';
|
|
4
|
+
import { PasswordIdentifierPolicy } from '@application/policies/password-identifier.policy.js';
|
|
5
|
+
import { PasswordPresentPolicy } from '@application/policies/password-present.policy.js';
|
|
6
|
+
import { PasswordStrengthPolicy } from '@application/policies/password-strength.policy.js';
|
|
7
|
+
import { AuthPreferencesServicePort } from '@application/ports/auth-preference-service.port.js';
|
|
8
|
+
import { AuthServicePort } from '@application/ports/auth-service.port.js';
|
|
9
|
+
import { PreRegisterPolicy } from '@application/ports/pre-register-policy.port.js';
|
|
10
|
+
import { UserExistencePort } from '@application/ports/user-existence.port.js';
|
|
11
|
+
import { authenticateUser } from '@application/use-cases/authenticate-user.js';
|
|
12
|
+
import { registerUser } from '@application/use-cases/register-user.js';
|
|
13
|
+
import { AuthEngine } from '@root/auth-engine.js';
|
|
14
|
+
|
|
15
|
+
export class AuthEngineFactory {
|
|
16
|
+
static async create(
|
|
17
|
+
authService: AuthServicePort,
|
|
18
|
+
authPreferencesService: AuthPreferencesServicePort,
|
|
19
|
+
userExistence: UserExistencePort,
|
|
20
|
+
basePolicies: PreRegisterPolicy[]
|
|
21
|
+
): Promise<AuthEngine> {
|
|
22
|
+
const preferences = await authPreferencesService.getAuthPreferences();
|
|
23
|
+
|
|
24
|
+
const policies = [
|
|
25
|
+
...basePolicies,
|
|
26
|
+
new AccountAlreadyExistsPolicy(
|
|
27
|
+
userExistence,
|
|
28
|
+
!preferences.allowDuplicatedAccount
|
|
29
|
+
),
|
|
30
|
+
new PasswordStrengthPolicy(
|
|
31
|
+
!preferences.allowWeakPassword
|
|
32
|
+
),
|
|
33
|
+
new PasswordIdentifierPolicy(
|
|
34
|
+
!preferences.allowMissingPasswordIdentifier
|
|
35
|
+
),
|
|
36
|
+
new PasswordPresentPolicy(
|
|
37
|
+
!preferences.allowMissingPassword
|
|
38
|
+
),
|
|
39
|
+
new OtpIdentifierPolicy(
|
|
40
|
+
!preferences.allowMissingOtpIdentifier
|
|
41
|
+
),
|
|
42
|
+
new OtpCodePolicy(
|
|
43
|
+
!preferences.allowMissingOtpCode
|
|
44
|
+
),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return new AuthEngine({
|
|
48
|
+
register: registerUser(authService, policies),
|
|
49
|
+
authenticate: authenticateUser(authService, authPreferencesService),
|
|
50
|
+
logout: () => authService.logout(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AuthMethod, AuthResult } from "@application/ports/auth-service.port.js";
|
|
2
|
+
|
|
3
|
+
export interface AuthActions {
|
|
4
|
+
register(method: AuthMethod): Promise<AuthResult>;
|
|
5
|
+
authenticate(method: AuthMethod): Promise<AuthResult>;
|
|
6
|
+
logout(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AuthEngine implements AuthActions {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly useCases: AuthActions,
|
|
12
|
+
) { }
|
|
13
|
+
|
|
14
|
+
async register(method: AuthMethod): Promise<AuthResult> {
|
|
15
|
+
return this.useCases.register(method);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async authenticate(method: AuthMethod): Promise<AuthResult> {
|
|
19
|
+
return await this.useCases.authenticate(method);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async logout(): Promise<void> {
|
|
23
|
+
return await this.useCases.logout();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
export type UserStatus = "active" | "disabled" | "lockedTemporarily";
|
|
2
2
|
|
|
3
|
-
export type UserRole = "parent" | "staff" | "admin" | "student" | "teacher";
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
4
|
* Domaine: compte utilisateur Sekoliko.
|
|
7
5
|
* Indépendant de toute technologie (pas de dépendance à Supabase ou React).
|
|
8
6
|
*/
|
|
9
7
|
export interface UserAccount {
|
|
10
8
|
id: string;
|
|
11
|
-
establishmentIds: string[];
|
|
12
9
|
displayName: string;
|
|
13
|
-
firstName?: string;
|
|
14
|
-
lastName?: string;
|
|
15
|
-
contactEmail: string;
|
|
16
10
|
status: UserStatus;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
failedLoginAttempts?: number;
|
|
12
|
+
lastFailedLoginAt?: Date | null;
|
|
13
|
+
isVerified?: boolean;
|
|
20
14
|
}
|
|
21
15
|
|
|
22
16
|
export const canLogin = (user: UserAccount): boolean =>
|
package/src/index.ts
CHANGED
|
@@ -2,10 +2,24 @@
|
|
|
2
2
|
* Domain
|
|
3
3
|
*/
|
|
4
4
|
export * from '@domain/user-account.js';
|
|
5
|
-
export * from '@domain/session.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Application
|
|
9
8
|
*/
|
|
10
|
-
export * from '@application/
|
|
9
|
+
export * from '@application/ports/auth-preference-service.port.js';
|
|
11
10
|
export * from '@application/ports/auth-service.port.js';
|
|
11
|
+
export * from '@application/ports/pre-register-policy.port.js';
|
|
12
|
+
export * from '@application/ports/user-existence.port.js';
|
|
13
|
+
export * from '@application/services/composite-auth.service.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Infrastructure
|
|
17
|
+
*/
|
|
18
|
+
// Infrastructure adapters are provided as separate packages
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Auth Engine
|
|
22
|
+
*/
|
|
23
|
+
export * from '@root/auth-engine.factory.js';
|
|
24
|
+
export * from '@root/auth-engine.js';
|
|
25
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MagicLinkTokenPolicy } from '@application/policies/magic-link-token.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('MagicLinkTokenPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new MagicLinkTokenPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'magic_link' as const,
|
|
9
|
+
token: 'short',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should return true when policy is enabled and token meets minimum length', async () => {
|
|
16
|
+
const policy = new MagicLinkTokenPolicy(true);
|
|
17
|
+
const method = {
|
|
18
|
+
type: 'magic_link' as const,
|
|
19
|
+
token: 'valid-token-123456789',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
23
|
+
expect(await policy.check(method)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return false when policy is enabled but token is missing', async () => {
|
|
27
|
+
const policy = new MagicLinkTokenPolicy(true);
|
|
28
|
+
const method = {
|
|
29
|
+
type: 'magic_link' as const,
|
|
30
|
+
token: '',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
34
|
+
expect(await policy.check(method)).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return false when policy is enabled but token is too short', async () => {
|
|
38
|
+
const policy = new MagicLinkTokenPolicy(true);
|
|
39
|
+
const method = {
|
|
40
|
+
type: 'magic_link' as const,
|
|
41
|
+
token: 'short', // 5 chars
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
45
|
+
expect(await policy.check(method)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should return false when policy is enabled but token is exactly at boundary', async () => {
|
|
49
|
+
const policy = new MagicLinkTokenPolicy(true);
|
|
50
|
+
const method = {
|
|
51
|
+
type: 'magic_link' as const,
|
|
52
|
+
token: '1234567890', // exactly 10 chars
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
56
|
+
expect(await policy.check(method)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return false for non-magic link methods', async () => {
|
|
60
|
+
const policy = new MagicLinkTokenPolicy(true);
|
|
61
|
+
const passwordMethod = {
|
|
62
|
+
type: 'password' as const,
|
|
63
|
+
identifier: 'test@example.com',
|
|
64
|
+
password: 'password123',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
expect(await policy.isActive(passwordMethod)).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should have correct error code', () => {
|
|
71
|
+
const policy = new MagicLinkTokenPolicy(true);
|
|
72
|
+
expect(policy.code).toBe('MISSING_MAGIC_LINK_TOKEN');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { OAuthCodePolicy } from '@application/policies/oauth-code.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('OAuthCodePolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new OAuthCodePolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'oauth' as const,
|
|
9
|
+
provider: 'google' as const,
|
|
10
|
+
code: '',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and code meets minimum length', async () => {
|
|
17
|
+
const policy = new OAuthCodePolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'oauth' as const,
|
|
20
|
+
provider: 'google' as const,
|
|
21
|
+
code: 'auth-code-12345',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but code is missing', async () => {
|
|
29
|
+
const policy = new OAuthCodePolicy(true);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'oauth' as const,
|
|
32
|
+
provider: 'google' as const,
|
|
33
|
+
code: '',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return false when policy is enabled but code is too short', async () => {
|
|
41
|
+
const policy = new OAuthCodePolicy(true);
|
|
42
|
+
const method = {
|
|
43
|
+
type: 'oauth' as const,
|
|
44
|
+
provider: 'google' as const,
|
|
45
|
+
code: 'five', // 5 chars (too short)
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
49
|
+
expect(await policy.check(method)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return true when policy is enabled and code is exactly at boundary', async () => {
|
|
53
|
+
const policy = new OAuthCodePolicy(true);
|
|
54
|
+
const method = {
|
|
55
|
+
type: 'oauth' as const,
|
|
56
|
+
provider: 'google' as const,
|
|
57
|
+
code: '12345', // exactly 5 chars
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
61
|
+
expect(await policy.check(method)).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return false for non-OAuth methods', async () => {
|
|
65
|
+
const policy = new OAuthCodePolicy(true);
|
|
66
|
+
const passwordMethod = {
|
|
67
|
+
type: 'password' as const,
|
|
68
|
+
identifier: 'test@example.com',
|
|
69
|
+
password: 'password123',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(await policy.isActive(passwordMethod)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should have correct error code', () => {
|
|
76
|
+
const policy = new OAuthCodePolicy(true);
|
|
77
|
+
expect(policy.code).toBe('MISSING_OAUTH_CODE');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { OAuthProviderPolicy } from '@application/policies/oauth-provider.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('OAuthProviderPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new OAuthProviderPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'oauth' as const,
|
|
9
|
+
provider: 'unsupported' as any,
|
|
10
|
+
code: 'auth-code',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and provider is supported', async () => {
|
|
17
|
+
const policy = new OAuthProviderPolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'oauth' as const,
|
|
20
|
+
provider: 'google' as const,
|
|
21
|
+
code: 'auth-code',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return true for all supported providers', async () => {
|
|
29
|
+
const policy = new OAuthProviderPolicy(true);
|
|
30
|
+
const supportedProviders = [
|
|
31
|
+
'google',
|
|
32
|
+
'github',
|
|
33
|
+
'apple',
|
|
34
|
+
'facebook',
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
for (const provider of supportedProviders) {
|
|
38
|
+
const method = {
|
|
39
|
+
type: 'oauth' as const,
|
|
40
|
+
provider,
|
|
41
|
+
code: 'auth-code',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
45
|
+
expect(await policy.check(method)).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return false when policy is enabled but provider is unsupported', async () => {
|
|
50
|
+
const policy = new OAuthProviderPolicy(true);
|
|
51
|
+
const method = {
|
|
52
|
+
type: 'oauth' as const,
|
|
53
|
+
provider: 'unsupported-provider' as any,
|
|
54
|
+
code: 'auth-code',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
58
|
+
expect(await policy.check(method)).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return false for non-OAuth methods', async () => {
|
|
62
|
+
const policy = new OAuthProviderPolicy(true);
|
|
63
|
+
const passwordMethod = {
|
|
64
|
+
type: 'password' as const,
|
|
65
|
+
identifier: 'test@example.com',
|
|
66
|
+
password: 'password123',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
expect(await policy.isActive(passwordMethod)).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should have correct error code', () => {
|
|
73
|
+
const policy = new OAuthProviderPolicy(true);
|
|
74
|
+
expect(policy.code).toBe('UNSUPPORTED_OAUTH_PROVIDER');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { OtpCodeFormatPolicy } from '@application/policies/otp-code-format.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('OtpCodeFormatPolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new OtpCodeFormatPolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'otp' as const,
|
|
9
|
+
identifier: 'test@example.com',
|
|
10
|
+
code: 'invalid',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and code has valid format', async () => {
|
|
17
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'otp' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
code: '123456',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but code format is invalid', async () => {
|
|
29
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'otp' as const,
|
|
32
|
+
identifier: 'test@example.com',
|
|
33
|
+
code: 'abc123', // Contains letters
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return false when policy is enabled but code is too short', async () => {
|
|
41
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
42
|
+
const method = {
|
|
43
|
+
type: 'otp' as const,
|
|
44
|
+
identifier: 'test@example.com',
|
|
45
|
+
code: '12345', // 5 digits
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
49
|
+
expect(await policy.check(method)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return false when policy is enabled but code is too long', async () => {
|
|
53
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
54
|
+
const method = {
|
|
55
|
+
type: 'otp' as const,
|
|
56
|
+
identifier: 'test@example.com',
|
|
57
|
+
code: '1234567', // 7 digits
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
61
|
+
expect(await policy.check(method)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle empty code string', async () => {
|
|
65
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
66
|
+
const method = {
|
|
67
|
+
type: 'otp' as const,
|
|
68
|
+
identifier: 'test@example.com',
|
|
69
|
+
code: '',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(await policy.check(method)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return false for non-OTP methods', async () => {
|
|
76
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
77
|
+
const passwordMethod = {
|
|
78
|
+
type: 'password' as const,
|
|
79
|
+
identifier: 'test@example.com',
|
|
80
|
+
password: 'password123',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(await policy.isActive(passwordMethod)).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have correct error code', () => {
|
|
87
|
+
const policy = new OtpCodeFormatPolicy(true);
|
|
88
|
+
expect(policy.code).toBe('INVALID_OTP_FORMAT');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { OtpCodePolicy } from '@application/policies/otp-code.policy.js';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('OtpCodePolicy', () => {
|
|
5
|
+
it('should return false when policy is disabled', async () => {
|
|
6
|
+
const policy = new OtpCodePolicy(false);
|
|
7
|
+
const method = {
|
|
8
|
+
type: 'otp' as const,
|
|
9
|
+
identifier: 'test@example.com',
|
|
10
|
+
code: '',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(await policy.isActive(method)).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return true when policy is enabled and code is present', async () => {
|
|
17
|
+
const policy = new OtpCodePolicy(true);
|
|
18
|
+
const method = {
|
|
19
|
+
type: 'otp' as const,
|
|
20
|
+
identifier: 'test@example.com',
|
|
21
|
+
code: '123456',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
25
|
+
expect(await policy.check(method)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return false when policy is enabled but code is missing', async () => {
|
|
29
|
+
const policy = new OtpCodePolicy(true);
|
|
30
|
+
const method = {
|
|
31
|
+
type: 'otp' as const,
|
|
32
|
+
identifier: 'test@example.com',
|
|
33
|
+
code: '',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
expect(await policy.isActive(method)).toBe(true);
|
|
37
|
+
expect(await policy.check(method)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return false for non-OTP methods', async () => {
|
|
41
|
+
const policy = new OtpCodePolicy(true);
|
|
42
|
+
const passwordMethod = {
|
|
43
|
+
type: 'password' as const,
|
|
44
|
+
identifier: 'test@example.com',
|
|
45
|
+
password: 'password123',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expect(await policy.isActive(passwordMethod)).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should have correct error code', () => {
|
|
52
|
+
const policy = new OtpCodePolicy(true);
|
|
53
|
+
expect(policy.code).toBe('MISSING_OTP_CODE');
|
|
54
|
+
});
|
|
55
|
+
});
|