@plyaz/auth 1.0.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/.github/pull_request_template.md +71 -0
- package/.github/workflows/deploy.yml +9 -0
- package/.github/workflows/publish.yml +14 -0
- package/.github/workflows/security.yml +20 -0
- package/README.md +89 -0
- package/commits.txt +5 -0
- package/dist/common/index.cjs +48 -0
- package/dist/common/index.cjs.map +1 -0
- package/dist/common/index.mjs +43 -0
- package/dist/common/index.mjs.map +1 -0
- package/dist/index.cjs +20411 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +5139 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +13 -0
- package/index.html +13 -0
- package/package.json +141 -0
- package/src/adapters/auth-adapter-factory.ts +26 -0
- package/src/adapters/auth-adapter.mapper.ts +53 -0
- package/src/adapters/base-auth.adapter.ts +119 -0
- package/src/adapters/clerk/clerk.adapter.ts +204 -0
- package/src/adapters/custom/custom.adapter.ts +119 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/next-auth/authOptions.ts +81 -0
- package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
- package/src/api/client.ts +37 -0
- package/src/audit/audit.logger.ts +52 -0
- package/src/client/components/ProtectedRoute.tsx +37 -0
- package/src/client/hooks/useAuth.ts +128 -0
- package/src/client/hooks/useConnectedAccounts.ts +108 -0
- package/src/client/hooks/usePermissions.ts +36 -0
- package/src/client/hooks/useRBAC.ts +36 -0
- package/src/client/hooks/useSession.ts +18 -0
- package/src/client/providers/AuthProvider.tsx +104 -0
- package/src/client/store/auth.store.ts +306 -0
- package/src/client/utils/storage.ts +70 -0
- package/src/common/constants/oauth-providers.ts +49 -0
- package/src/common/errors/auth.errors.ts +64 -0
- package/src/common/errors/specific-auth-errors.ts +201 -0
- package/src/common/index.ts +19 -0
- package/src/common/regex/index.ts +27 -0
- package/src/common/types/auth.types.ts +641 -0
- package/src/common/types/index.ts +297 -0
- package/src/common/utils/index.ts +84 -0
- package/src/core/blacklist/token.blacklist.ts +60 -0
- package/src/core/index.ts +2 -0
- package/src/core/jwt/jwt.manager.ts +131 -0
- package/src/core/session/session.manager.ts +56 -0
- package/src/db/repositories/connected-account.repository.ts +415 -0
- package/src/db/repositories/role.repository.ts +519 -0
- package/src/db/repositories/session.repository.ts +308 -0
- package/src/db/repositories/user.repository.ts +320 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/sign-in.flow.ts +106 -0
- package/src/flows/sign-up.flow.ts +121 -0
- package/src/index.ts +54 -0
- package/src/libs/clerk.helper.ts +36 -0
- package/src/libs/supabase.helper.ts +255 -0
- package/src/libs/supabaseClient.ts +6 -0
- package/src/providers/base/auth-provider.interface.ts +42 -0
- package/src/providers/base/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/oauth/facebook.provider.ts +97 -0
- package/src/providers/oauth/github.provider.ts +148 -0
- package/src/providers/oauth/google.provider.ts +126 -0
- package/src/providers/oauth/index.ts +3 -0
- package/src/rbac/dynamic-roles.ts +552 -0
- package/src/rbac/index.ts +4 -0
- package/src/rbac/permission-checker.ts +464 -0
- package/src/rbac/role-hierarchy.ts +545 -0
- package/src/rbac/role.manager.ts +75 -0
- package/src/security/csrf/csrf.protection.ts +37 -0
- package/src/security/index.ts +3 -0
- package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
- package/src/security/rate-limiting/auth.module.ts +32 -0
- package/src/server/auth.module.ts +158 -0
- package/src/server/decorators/auth.decorator.ts +43 -0
- package/src/server/decorators/auth.decorators.ts +31 -0
- package/src/server/decorators/current-user.decorator.ts +49 -0
- package/src/server/decorators/permission.decorator.ts +49 -0
- package/src/server/guards/auth.guard.ts +56 -0
- package/src/server/guards/custom-throttler.guard.ts +46 -0
- package/src/server/guards/permissions.guard.ts +115 -0
- package/src/server/guards/roles.guard.ts +31 -0
- package/src/server/middleware/auth.middleware.ts +46 -0
- package/src/server/middleware/index.ts +2 -0
- package/src/server/middleware/middleware.ts +11 -0
- package/src/server/middleware/session.middleware.ts +255 -0
- package/src/server/services/account.service.ts +269 -0
- package/src/server/services/auth.service.ts +79 -0
- package/src/server/services/brute-force.service.ts +98 -0
- package/src/server/services/index.ts +15 -0
- package/src/server/services/rate-limiter.service.ts +60 -0
- package/src/server/services/session.service.ts +287 -0
- package/src/server/services/token.service.ts +262 -0
- package/src/session/cookie-store.ts +255 -0
- package/src/session/enhanced-session-manager.ts +406 -0
- package/src/session/index.ts +14 -0
- package/src/session/memory-store.ts +320 -0
- package/src/session/redis-store.ts +443 -0
- package/src/strategies/oauth.strategy.ts +128 -0
- package/src/strategies/traditional-auth.strategy.ts +116 -0
- package/src/tokens/index.ts +4 -0
- package/src/tokens/refresh-token-manager.ts +448 -0
- package/src/tokens/token-validator.ts +311 -0
- package/tsconfig.build.json +28 -0
- package/tsconfig.json +38 -0
- package/tsup.config.mjs +28 -0
- package/vitest.config.mjs +16 -0
- package/vitest.setup.d.ts +2 -0
- package/vitest.setup.d.ts.map +1 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sign-in authentication flow
|
|
3
|
+
* @module @plyaz/auth/flows/sign-in
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NUMERIX } from "@plyaz/config";
|
|
7
|
+
import type { AuthTokens, AuthUser } from "@plyaz/types";
|
|
8
|
+
|
|
9
|
+
export interface SignInCredentials {
|
|
10
|
+
email: string;
|
|
11
|
+
password: string;
|
|
12
|
+
rememberMe?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SignInResult {
|
|
16
|
+
user: AuthUser;
|
|
17
|
+
tokens: AuthTokens;
|
|
18
|
+
requiresMFA?: boolean;
|
|
19
|
+
mfaToken?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** User repository type based on usage */
|
|
23
|
+
export interface UserRepositorySignin {
|
|
24
|
+
findByEmail(email: string): Promise<AuthUser | null>;
|
|
25
|
+
findById(userId: string): Promise<AuthUser | null>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** JWT manager type based on usage */
|
|
29
|
+
export interface JWTManagerSignin {
|
|
30
|
+
generateTokens(userId: string): Promise<AuthTokens>;
|
|
31
|
+
verifyToken(token: string): Promise<{ userId: string }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Session manager type based on usage */
|
|
35
|
+
export interface SessionManagerSignin {
|
|
36
|
+
createSession(session: { userId: string; expiresAt: Date; metadata?: Record<string, true> }): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Password service type based on usage */
|
|
40
|
+
export interface PasswordServiceSignin {
|
|
41
|
+
verify(password: string, passwordHash: string | undefined): Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SignInFlow {
|
|
45
|
+
constructor(
|
|
46
|
+
private userRepository: UserRepositorySignin,
|
|
47
|
+
private jwtManager: JWTManagerSignin,
|
|
48
|
+
private sessionManager: SessionManagerSignin,
|
|
49
|
+
private passwordService: PasswordServiceSignin
|
|
50
|
+
) {}
|
|
51
|
+
|
|
52
|
+
async execute(credentials: SignInCredentials): Promise<SignInResult> {
|
|
53
|
+
const isValid = await this.validateCredentials(credentials.email, credentials.password);
|
|
54
|
+
if (!isValid) {
|
|
55
|
+
throw new Error('Invalid credentials');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const user = await this.userRepository.findByEmail(credentials.email);
|
|
59
|
+
if (!user) {
|
|
60
|
+
throw new Error('User not found');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tokens = await this.jwtManager.generateTokens(user.id);
|
|
64
|
+
|
|
65
|
+
if (credentials.rememberMe) {
|
|
66
|
+
const thirty = 30;
|
|
67
|
+
await this.sessionManager.createSession({
|
|
68
|
+
userId: user.id,
|
|
69
|
+
expiresAt: new Date(Date.now() + thirty * NUMERIX.TWENTY_FOUR * NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.THOUSAND), // 30 days
|
|
70
|
+
metadata: { rememberMe: true }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { user, tokens };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async validateCredentials(email: string, password: string): Promise<boolean> {
|
|
78
|
+
const user = await this.userRepository.findByEmail(email);
|
|
79
|
+
if (!user) return false;
|
|
80
|
+
|
|
81
|
+
return await this.passwordService.verify(password, user.passwordHash);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async handleMFA(mfaToken: string, code: string): Promise<SignInResult> {
|
|
85
|
+
const payload = await this.jwtManager.verifyToken(mfaToken);
|
|
86
|
+
const user = await this.userRepository.findById(payload.userId);
|
|
87
|
+
|
|
88
|
+
if (!user) {
|
|
89
|
+
throw new Error('User not found');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isValidCode = await this.verifyMFACode(user.id, code);
|
|
93
|
+
if (!isValidCode) {
|
|
94
|
+
throw new Error('Invalid MFA code');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const tokens = await this.jwtManager.generateTokens(user.id);
|
|
98
|
+
return { user, tokens };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async verifyMFACode(userId: string, code: string): Promise<boolean> {
|
|
102
|
+
globalThis.console.log(userId,code)
|
|
103
|
+
|
|
104
|
+
return true; // Placeholder
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Sign-up authentication flow
|
|
3
|
+
* @module @plyaz/auth/flows/sign-up
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthTokens, AuthUser } from '@plyaz/types';
|
|
7
|
+
|
|
8
|
+
export interface SignUpData {
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
firstName?: string;
|
|
12
|
+
lastName?: string;
|
|
13
|
+
metadata?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SignUpResult {
|
|
17
|
+
user: AuthUser;
|
|
18
|
+
tokens: AuthTokens;
|
|
19
|
+
requiresVerification?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** User repository type based on usage */
|
|
23
|
+
export interface UserRepositorySignUp {
|
|
24
|
+
findByEmail(email: string): Promise<AuthUser | null>;
|
|
25
|
+
create(data: {
|
|
26
|
+
email: string;
|
|
27
|
+
passwordHash: string;
|
|
28
|
+
firstName?: string;
|
|
29
|
+
lastName?: string;
|
|
30
|
+
metadata?: Record<string, string >;
|
|
31
|
+
emailVerified: boolean;
|
|
32
|
+
}): Promise<AuthUser>;
|
|
33
|
+
findById(userId: string): Promise<AuthUser>;
|
|
34
|
+
updateEmailVerification(userId: string, verified: boolean): Promise<AuthUser | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** JWT manager type based on usage */
|
|
38
|
+
export interface JWTManagerSignUp {
|
|
39
|
+
generateTokens(userId: string): Promise<AuthTokens>;
|
|
40
|
+
generateVerificationToken(userId: string): Promise<string>;
|
|
41
|
+
verifyToken(token: string): Promise<{ userId: string }>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Password service type based on usage */
|
|
45
|
+
export interface PasswordServiceSignUp {
|
|
46
|
+
hash(password: string): Promise<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Email service type based on usage */
|
|
50
|
+
export interface EmailService {
|
|
51
|
+
sendVerificationEmail(args: { to: string; token: string; userId: string }): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class SignUpFlow {
|
|
55
|
+
constructor(
|
|
56
|
+
private userRepository: UserRepositorySignUp,
|
|
57
|
+
private jwtManager: JWTManagerSignUp,
|
|
58
|
+
private passwordService: PasswordServiceSignUp,
|
|
59
|
+
private emailService: EmailService
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
async execute(data: SignUpData): Promise<SignUpResult> {
|
|
63
|
+
const emailExists = await this.userRepository.findByEmail(data.email);
|
|
64
|
+
if (emailExists) {
|
|
65
|
+
throw new Error('Email already exists');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isValidEmail = await this.validateEmail(data.email);
|
|
69
|
+
if (!isValidEmail) {
|
|
70
|
+
throw new Error('Invalid email format');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const passwordHash = await this.passwordService.hash(data.password);
|
|
74
|
+
|
|
75
|
+
const user = await this.userRepository.create({
|
|
76
|
+
email: data.email,
|
|
77
|
+
passwordHash,
|
|
78
|
+
firstName: data.firstName,
|
|
79
|
+
lastName: data.lastName,
|
|
80
|
+
metadata: data.metadata,
|
|
81
|
+
emailVerified: false
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const tokens = await this.jwtManager.generateTokens(user.id);
|
|
85
|
+
|
|
86
|
+
await this.sendVerificationEmail(user.id);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
user,
|
|
90
|
+
tokens,
|
|
91
|
+
requiresVerification: true
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async validateEmail(email: string): Promise<boolean> {
|
|
96
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
97
|
+
return emailRegex.test(email);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async sendVerificationEmail(userId: string): Promise<void> {
|
|
101
|
+
const verificationToken = await this.jwtManager.generateVerificationToken(userId);
|
|
102
|
+
const user = await this.userRepository.findById(userId);
|
|
103
|
+
|
|
104
|
+
await this.emailService.sendVerificationEmail({
|
|
105
|
+
to: user.email,
|
|
106
|
+
token: verificationToken,
|
|
107
|
+
userId
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async verifyEmail(token: string): Promise<AuthUser> {
|
|
112
|
+
const payload = await this.jwtManager.verifyToken(token);
|
|
113
|
+
const user = await this.userRepository.updateEmailVerification(payload.userId, true);
|
|
114
|
+
|
|
115
|
+
if (!user) {
|
|
116
|
+
throw new Error('User not found');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return user;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Common (Enums, Interfaces, Types, Constants, Errors, Regex)
|
|
2
|
+
export * from './common';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// Core Authentication
|
|
7
|
+
export * from './core';
|
|
8
|
+
export * from './rbac';
|
|
9
|
+
export * from './security';
|
|
10
|
+
export * from './audit/audit.logger';
|
|
11
|
+
|
|
12
|
+
// Session Management
|
|
13
|
+
export * from './session';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// Adapters
|
|
18
|
+
export * from './adapters';
|
|
19
|
+
|
|
20
|
+
// Strategies
|
|
21
|
+
export * from './strategies/traditional-auth.strategy';
|
|
22
|
+
export * from './strategies/oauth.strategy';
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// Providers
|
|
27
|
+
export * from './providers';
|
|
28
|
+
|
|
29
|
+
// Flows
|
|
30
|
+
export * from './flows';
|
|
31
|
+
|
|
32
|
+
// Backend (NestJS)
|
|
33
|
+
export * from './server/services';
|
|
34
|
+
export * from './server/guards/auth.guard';
|
|
35
|
+
export * from './server/guards/roles.guard';
|
|
36
|
+
export * from './server/guards/permissions.guard';
|
|
37
|
+
export { Auth, Public } from './server/decorators/auth.decorator';
|
|
38
|
+
export { Roles, Permissions, CurrentSession } from './server/decorators/auth.decorators';
|
|
39
|
+
export { CurrentUser } from './server/decorators/current-user.decorator';
|
|
40
|
+
export { Permission as PermissionDecorator, Permissions as PermissionsDecorator, AnyPermission } from './server/decorators/permission.decorator';
|
|
41
|
+
export * from './server/middleware';
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
// Frontend (React)
|
|
46
|
+
export { AuthProvider } from './client/providers/AuthProvider';
|
|
47
|
+
export { useAuth } from './client/hooks/useAuth';
|
|
48
|
+
export { useSession } from './client/hooks/useSession';
|
|
49
|
+
export { useRBAC } from './client/hooks/useRBAC';
|
|
50
|
+
export { useConnectedAccounts } from './client/hooks/useConnectedAccounts';
|
|
51
|
+
export { usePermissions } from './client/hooks/usePermissions';
|
|
52
|
+
export { useAuthStore } from './client/store/auth.store';
|
|
53
|
+
export { ProtectedRoute } from './client/components/ProtectedRoute';
|
|
54
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
import { createClerkClient } from "@clerk/backend";
|
|
3
|
+
import { BaseError, DatabasePackageError } from "@plyaz/errors";
|
|
4
|
+
|
|
5
|
+
// Validate env early (VERY important)
|
|
6
|
+
if (!globalThis.process.env.CLERK_SECRET_KEY) {
|
|
7
|
+
throw new BaseError('CLIENT_MISSING_BASE_URL');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Create Clerk client
|
|
11
|
+
const clerkClient = createClerkClient({
|
|
12
|
+
secretKey: globalThis.process.env.CLERK_SECRET_KEY,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
globalThis.console.log("CLERK_SECRET_KEY loaded");
|
|
16
|
+
|
|
17
|
+
// Create OAuth Application
|
|
18
|
+
export const createClerkUser
|
|
19
|
+
= async ():Promise<void> =>{
|
|
20
|
+
try {
|
|
21
|
+
globalThis.console.log("Creating OAuth application...");
|
|
22
|
+
|
|
23
|
+
const response = await clerkClient.users.createUser({
|
|
24
|
+
firstName: "Test",
|
|
25
|
+
lastName: "User",
|
|
26
|
+
emailAddress: ["testclerk123@gmail.com"],
|
|
27
|
+
password: "!Testing233sw@qsdfefaf123!",
|
|
28
|
+
});
|
|
29
|
+
globalThis.console.dir("Full Response:", response);
|
|
30
|
+
} catch (error: unknown) {
|
|
31
|
+
if (error instanceof Error) {
|
|
32
|
+
throw new DatabasePackageError(error.message);
|
|
33
|
+
}
|
|
34
|
+
throw new DatabasePackageError('An unknown error occurred');
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { AUTHPROVIDER, AuthUser, ConnectedAccount, Session } from '@plyaz/types';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import bcrypt from 'bcryptjs';
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
import { supabase } from './supabaseClient';
|
|
7
|
+
import { NUMERIX } from '@plyaz/config';
|
|
8
|
+
|
|
9
|
+
export interface DeviceInfo {
|
|
10
|
+
ip: string;
|
|
11
|
+
browser: string;
|
|
12
|
+
os: string;
|
|
13
|
+
userAgent: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a new user
|
|
18
|
+
*/
|
|
19
|
+
export async function createUser(
|
|
20
|
+
email: string,
|
|
21
|
+
authProvider: string,
|
|
22
|
+
data?: Partial<AuthUser>,
|
|
23
|
+
password?: string
|
|
24
|
+
): Promise<AuthUser> {
|
|
25
|
+
const passwordHash = password ? await bcrypt.hash(password, NUMERIX.TEN) : undefined;
|
|
26
|
+
|
|
27
|
+
const { data: user, error } = await supabase
|
|
28
|
+
.from('users')
|
|
29
|
+
.insert({
|
|
30
|
+
email,
|
|
31
|
+
auth_provider: authProvider,
|
|
32
|
+
first_name: data?.firstName,
|
|
33
|
+
last_name: data?.lastName,
|
|
34
|
+
display_name: data?.displayName ?? email,
|
|
35
|
+
avatar_url: data?.avatarUrl,
|
|
36
|
+
phone_number: data?.phoneNumber,
|
|
37
|
+
is_active: true,
|
|
38
|
+
is_verified: authProvider !== 'email', // email signups need verification
|
|
39
|
+
roles: data?.roles ?? [],
|
|
40
|
+
password_hash: passwordHash,
|
|
41
|
+
})
|
|
42
|
+
.select('*')
|
|
43
|
+
.single();
|
|
44
|
+
|
|
45
|
+
if (error || !user) throw new Error(error?.message ?? 'Failed to create user');
|
|
46
|
+
return user;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find user by email + provider
|
|
51
|
+
*/
|
|
52
|
+
export async function findUserByEmailProvider(
|
|
53
|
+
email: string,
|
|
54
|
+
provider: string
|
|
55
|
+
): Promise<AuthUser | null> {
|
|
56
|
+
const { data, error } = await supabase
|
|
57
|
+
.from('users')
|
|
58
|
+
.select('*')
|
|
59
|
+
.eq('email', email)
|
|
60
|
+
.eq('auth_provider', provider)
|
|
61
|
+
.single();
|
|
62
|
+
|
|
63
|
+
if (error || !data) return null;
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update last login timestamp
|
|
69
|
+
*/
|
|
70
|
+
export async function updateLastLogin(userId: string):Promise<void>{
|
|
71
|
+
await supabase
|
|
72
|
+
.from('users')
|
|
73
|
+
.update({ last_login_at: new Date() })
|
|
74
|
+
.eq('id', userId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Create a new session
|
|
79
|
+
*/
|
|
80
|
+
export async function createUserSession(
|
|
81
|
+
userId: string,
|
|
82
|
+
deviceInfo: DeviceInfo
|
|
83
|
+
): Promise<Session & { accessToken: string }> {
|
|
84
|
+
const thirtyTwo = 32;
|
|
85
|
+
const accessToken = randomBytes(thirtyTwo).toString('hex');
|
|
86
|
+
const tokenHash = await bcrypt.hash(accessToken, NUMERIX.TEN);
|
|
87
|
+
const expiresAt = new Date(Date.now() + NUMERIX.THOUSAND * NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.TWENTY_FOUR); // 24h
|
|
88
|
+
|
|
89
|
+
const { data, error } = await supabase
|
|
90
|
+
.from('sessions')
|
|
91
|
+
.insert({
|
|
92
|
+
user_id: userId,
|
|
93
|
+
token_hash: tokenHash,
|
|
94
|
+
device_info: deviceInfo,
|
|
95
|
+
expires_at: expiresAt,
|
|
96
|
+
last_active_at: new Date(),
|
|
97
|
+
})
|
|
98
|
+
.select('*')
|
|
99
|
+
.single();
|
|
100
|
+
|
|
101
|
+
if (error || !data) throw new Error(error?.message ?? 'Failed to create session');
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id: data.id,
|
|
105
|
+
userId: data.user_id,
|
|
106
|
+
expiresAt: data.expires_at,
|
|
107
|
+
provider: data.provider,
|
|
108
|
+
createdAt: data.created_at,
|
|
109
|
+
lastActivityAt: data.last_active_at,
|
|
110
|
+
accessToken
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get a session by ID
|
|
116
|
+
*/
|
|
117
|
+
export async function getSession(sessionId: string): Promise<Session | null> {
|
|
118
|
+
const { data, error } = await supabase
|
|
119
|
+
.from('sessions')
|
|
120
|
+
.select('*')
|
|
121
|
+
.eq('id', sessionId)
|
|
122
|
+
.single();
|
|
123
|
+
|
|
124
|
+
if (error || !data) return null;
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
id: data.id,
|
|
128
|
+
userId: data.user_id,
|
|
129
|
+
expiresAt: data.expires_at,
|
|
130
|
+
provider: data.provider,
|
|
131
|
+
createdAt: data.created_at,
|
|
132
|
+
lastActivityAt: data.last_active_at
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate session (exists + not expired)
|
|
138
|
+
*/
|
|
139
|
+
export async function validateSessionDB(sessionId: string): Promise<boolean> {
|
|
140
|
+
const { data } = await supabase
|
|
141
|
+
.from('sessions')
|
|
142
|
+
.select('id')
|
|
143
|
+
.eq('id', sessionId)
|
|
144
|
+
.gte('expires_at', new Date())
|
|
145
|
+
.single();
|
|
146
|
+
|
|
147
|
+
return !!data;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Delete session
|
|
152
|
+
*/
|
|
153
|
+
export async function deleteSession(sessionId: string):Promise<void> {
|
|
154
|
+
await supabase.from('sessions').delete().eq('id', sessionId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Refresh session
|
|
159
|
+
*/
|
|
160
|
+
export async function refreshSessionDB(sessionId: string): Promise<Session & { accessToken: string }> {
|
|
161
|
+
const thirtyTwo = 32;
|
|
162
|
+
const accessToken = randomBytes(thirtyTwo).toString('hex');
|
|
163
|
+
const tokenHash = await bcrypt.hash(accessToken, NUMERIX.TEN);
|
|
164
|
+
const expiresAt = new Date(Date.now() + NUMERIX.THOUSAND * NUMERIX.SIXTY * NUMERIX.SIXTY * NUMERIX.TWENTY_FOUR); // 24h
|
|
165
|
+
|
|
166
|
+
const { data, error } = await supabase
|
|
167
|
+
.from('sessions')
|
|
168
|
+
.update({
|
|
169
|
+
token_hash: tokenHash,
|
|
170
|
+
expires_at: expiresAt,
|
|
171
|
+
last_active_at: new Date(),
|
|
172
|
+
})
|
|
173
|
+
.eq('id', sessionId)
|
|
174
|
+
.select('*')
|
|
175
|
+
.single();
|
|
176
|
+
|
|
177
|
+
if (error || !data) throw new Error(error?.message ?? 'Failed to refresh session');
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
id: data.id,
|
|
181
|
+
userId: data.user_id,
|
|
182
|
+
expiresAt: data.expires_at,
|
|
183
|
+
provider: data.provider,
|
|
184
|
+
createdAt: data.created_at,
|
|
185
|
+
lastActivityAt: data.last_active_at,
|
|
186
|
+
accessToken
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
export async function linkConnectedAccount(
|
|
192
|
+
userId: string,
|
|
193
|
+
provider:AUTHPROVIDER,
|
|
194
|
+
data: ConnectedAccount
|
|
195
|
+
) :Promise<ConnectedAccount>{
|
|
196
|
+
const { data: account, error } = await supabase
|
|
197
|
+
.from('connected_accounts')
|
|
198
|
+
.insert({
|
|
199
|
+
user_id: userId,
|
|
200
|
+
provider_type: data.providerType,
|
|
201
|
+
provider: provider,
|
|
202
|
+
provider_account_id: data.providerAccountId,
|
|
203
|
+
provider_email: data.providerEmail,
|
|
204
|
+
provider_username: data.providerUsername,
|
|
205
|
+
provider_display_name: data.providerDisplayName,
|
|
206
|
+
provider_avatar_url: data.providerAvatarUrl,
|
|
207
|
+
provider_profile_url: data.providerProfileUrl,
|
|
208
|
+
provider_metadata: data.providerMetadata ?? {},
|
|
209
|
+
wallet_address: data.walletAddress,
|
|
210
|
+
chain_id: data.chainId,
|
|
211
|
+
access_token_encrypted: data.accessTokenEncrypted,
|
|
212
|
+
refresh_token_encrypted: data.refreshTokenEncrypted,
|
|
213
|
+
token_expires_at: data.tokenExpiresAt,
|
|
214
|
+
token_scope: data.tokenScope,
|
|
215
|
+
is_primary: data.isPrimary ?? false,
|
|
216
|
+
is_verified: data.isVerified ?? true,
|
|
217
|
+
is_active: data.isActive ?? true,
|
|
218
|
+
linked_ip_address: data.linkedIpAddress,
|
|
219
|
+
linked_user_agent: data.linkedUserAgent,
|
|
220
|
+
})
|
|
221
|
+
.select('*')
|
|
222
|
+
.single();
|
|
223
|
+
|
|
224
|
+
if (error) throw new Error(error.message);
|
|
225
|
+
return account;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Unlink a provider account from a user
|
|
230
|
+
*/
|
|
231
|
+
export async function unlinkConnectedAccount(
|
|
232
|
+
userId: string,
|
|
233
|
+
accountId: string
|
|
234
|
+
):Promise<void>{
|
|
235
|
+
const { error } = await supabase
|
|
236
|
+
.from('connected_accounts')
|
|
237
|
+
.delete()
|
|
238
|
+
.eq('user_id', userId)
|
|
239
|
+
.eq('id', accountId);
|
|
240
|
+
|
|
241
|
+
if (error) throw new Error(error.message);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get all linked accounts for a user
|
|
246
|
+
*/
|
|
247
|
+
export async function getLinkedAccounts(userId: string):Promise<ConnectedAccount[] >{
|
|
248
|
+
const { data, error } = await supabase
|
|
249
|
+
.from('connected_accounts')
|
|
250
|
+
.select('*')
|
|
251
|
+
.eq('user_id', userId);
|
|
252
|
+
|
|
253
|
+
if (error) throw new Error(error.message);
|
|
254
|
+
return data || [];
|
|
255
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base authentication provider interface
|
|
3
|
+
* @module @plyaz/auth/providers/base/auth-provider
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AuthTokens, AuthUser } from "@plyaz/types";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
export interface AuthProviderConfig {
|
|
11
|
+
clientId: string;
|
|
12
|
+
clientSecret: string;
|
|
13
|
+
redirectUri: string;
|
|
14
|
+
scopes?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AuthProvider {
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly type: 'oauth' | 'traditional' | 'web3';
|
|
20
|
+
|
|
21
|
+
initialize(config: AuthProviderConfig): Promise<void>;
|
|
22
|
+
authenticate(credentials: {code:string}): Promise<{ user: AuthUser; tokens: AuthTokens }>;
|
|
23
|
+
refreshToken(refreshToken: string): Promise<AuthTokens>;
|
|
24
|
+
revokeToken(token: string): Promise<void>;
|
|
25
|
+
getUserProfile(accessToken: string): Promise<AuthUser>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export abstract class BaseAuthProvider implements AuthProvider {
|
|
29
|
+
abstract readonly name: string;
|
|
30
|
+
abstract readonly type: 'oauth' | 'traditional' | 'web3';
|
|
31
|
+
|
|
32
|
+
protected config?: AuthProviderConfig;
|
|
33
|
+
|
|
34
|
+
async initialize(config: AuthProviderConfig): Promise<void> {
|
|
35
|
+
this.config = config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
abstract authenticate(credentials: {code:string}): Promise<{ user: AuthUser; tokens: AuthTokens }>;
|
|
39
|
+
abstract refreshToken(refreshToken: string): Promise<AuthTokens>;
|
|
40
|
+
abstract revokeToken(token: string): Promise<void>;
|
|
41
|
+
abstract getUserProfile(accessToken: string): Promise<AuthUser>;
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./auth-provider.interface"
|