@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,81 @@
|
|
|
1
|
+
import type { NextAuthOptions } from 'next-auth';
|
|
2
|
+
import CredentialsProvider from 'next-auth/providers/credentials';
|
|
3
|
+
|
|
4
|
+
import { compare } from 'bcryptjs';
|
|
5
|
+
import { supabase } from '@/libs/supabaseClient';
|
|
6
|
+
import { NUMERIX } from '@plyaz/config';
|
|
7
|
+
|
|
8
|
+
const seven = 7;
|
|
9
|
+
export const authOptions: NextAuthOptions = {
|
|
10
|
+
providers: [
|
|
11
|
+
// Example: Email + password login (custom)
|
|
12
|
+
CredentialsProvider({
|
|
13
|
+
name: 'Credentials',
|
|
14
|
+
credentials: {
|
|
15
|
+
email: { label: 'Email', type: 'text', placeholder: 'email@example.com' },
|
|
16
|
+
password: { label: 'Password', type: 'password' },
|
|
17
|
+
},
|
|
18
|
+
async authorize(credentials) {
|
|
19
|
+
if (!credentials?.email || !credentials.password) return null;
|
|
20
|
+
|
|
21
|
+
// Fetch user from Supabase
|
|
22
|
+
const { data: user, error } = await supabase
|
|
23
|
+
.from('users')
|
|
24
|
+
.select('*')
|
|
25
|
+
.eq('email', credentials.email)
|
|
26
|
+
.single();
|
|
27
|
+
|
|
28
|
+
if (error || !user) return null;
|
|
29
|
+
|
|
30
|
+
// Compare password
|
|
31
|
+
const isValid = await compare(credentials.password, user.password_hash);
|
|
32
|
+
if (!isValid) return null;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
id: user.id,
|
|
36
|
+
email: user.email,
|
|
37
|
+
name: user.name,
|
|
38
|
+
role: user.role ?? 'user',
|
|
39
|
+
isVerified: user.is_verified,
|
|
40
|
+
isActive: user.is_active,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
],
|
|
45
|
+
|
|
46
|
+
// Session strategy: JWT (stateless)
|
|
47
|
+
session: {
|
|
48
|
+
strategy: 'jwt',
|
|
49
|
+
maxAge: seven * NUMERIX.TWENTY_FOUR * NUMERIX.SIXTY * NUMERIX.SIXTY, // 7 days
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// JWT settings
|
|
53
|
+
jwt: {
|
|
54
|
+
secret: globalThis.process.env.NEXTAUTH_SECRET,
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// callbacks: {
|
|
58
|
+
// async jwt({ token, user }) {
|
|
59
|
+
// if (user) {
|
|
60
|
+
// token.id = user.id;
|
|
61
|
+
// token.roles = user.roles;
|
|
62
|
+
// token.isVerified = user.is_verified;
|
|
63
|
+
// }
|
|
64
|
+
// return token;
|
|
65
|
+
// },
|
|
66
|
+
// async session({ session, token }) {
|
|
67
|
+
// if (token) {
|
|
68
|
+
// session.user.id = token.id as string;
|
|
69
|
+
// session.user.role = token.role as string;
|
|
70
|
+
// session.user.isVerified = token.isVerified as boolean;
|
|
71
|
+
// }
|
|
72
|
+
// return session;
|
|
73
|
+
// },
|
|
74
|
+
// },
|
|
75
|
+
|
|
76
|
+
pages: {
|
|
77
|
+
signIn: '/auth/signin', // custom signin page
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
debug: globalThis.process.env.NODE_ENV === 'development',
|
|
81
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
|
|
3
|
+
import { DatabasePackageError } from "@plyaz/errors";
|
|
4
|
+
import type {
|
|
5
|
+
AuthAdapterUser,
|
|
6
|
+
AuthDeviceInfo,
|
|
7
|
+
AUTHPROVIDER,
|
|
8
|
+
AuthProviderAdapter as BaseAuthProvider,
|
|
9
|
+
AuthSession,
|
|
10
|
+
AuthUser,
|
|
11
|
+
ConnectedAccount,
|
|
12
|
+
Credentials,
|
|
13
|
+
Tokens,
|
|
14
|
+
} from "@plyaz/types";
|
|
15
|
+
|
|
16
|
+
import type { DeviceInfo } from "../../libs/supabase.helper";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
createUser,
|
|
20
|
+
createUserSession,
|
|
21
|
+
findUserByEmailProvider,
|
|
22
|
+
getLinkedAccounts,
|
|
23
|
+
linkConnectedAccount,
|
|
24
|
+
unlinkConnectedAccount,
|
|
25
|
+
} from "../../libs/supabase.helper";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extends base provider WITHOUT changing behavior
|
|
29
|
+
*/
|
|
30
|
+
export interface AuthProviderAdapter extends BaseAuthProvider {
|
|
31
|
+
signIn(
|
|
32
|
+
credentials: unknown
|
|
33
|
+
): Promise<{
|
|
34
|
+
user: AuthAdapterUser;
|
|
35
|
+
session: AuthSession;
|
|
36
|
+
tokens: Tokens;
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class NextAuthAdapter implements AuthProviderAdapter {
|
|
41
|
+
// -------------------
|
|
42
|
+
// Identity
|
|
43
|
+
// -------------------
|
|
44
|
+
async signIn(
|
|
45
|
+
provider: AUTHPROVIDER,
|
|
46
|
+
credentials?: Credentials,
|
|
47
|
+
deviceInfo?: AuthDeviceInfo
|
|
48
|
+
): Promise<{
|
|
49
|
+
user: AuthAdapterUser;
|
|
50
|
+
session: AuthSession;
|
|
51
|
+
tokens: Tokens;
|
|
52
|
+
}> {
|
|
53
|
+
const userFindByEmail = await findUserByEmailProvider(
|
|
54
|
+
credentials?.email ?? "",
|
|
55
|
+
provider
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!userFindByEmail) {
|
|
59
|
+
return {
|
|
60
|
+
user: { id: "", email: "" },
|
|
61
|
+
session: { id: "", userId: "", expiresAt: new Date() },
|
|
62
|
+
tokens: { accessToken: "" },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const accounts = await getLinkedAccounts(userFindByEmail.id);
|
|
67
|
+
|
|
68
|
+
if (accounts.length === 0) {
|
|
69
|
+
return {
|
|
70
|
+
user: { id: userFindByEmail.id, email: userFindByEmail.email },
|
|
71
|
+
session: { id: "", userId: "", expiresAt: new Date() },
|
|
72
|
+
tokens: { accessToken: "" },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rawSession = await createUserSession(
|
|
77
|
+
accounts[0].id,
|
|
78
|
+
deviceInfo ?? {
|
|
79
|
+
ip: "",
|
|
80
|
+
browser: "",
|
|
81
|
+
os: "",
|
|
82
|
+
userAgent: "",
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const session: AuthSession = {
|
|
87
|
+
id: rawSession.id,
|
|
88
|
+
userId: rawSession.userId,
|
|
89
|
+
expiresAt: rawSession.expiresAt,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
user: {
|
|
94
|
+
id: accounts[0].id,
|
|
95
|
+
email: accounts[0].providerEmail ?? '',
|
|
96
|
+
},
|
|
97
|
+
session,
|
|
98
|
+
tokens: { accessToken: "jwt-token" },
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async signUp(
|
|
103
|
+
provider: AUTHPROVIDER,
|
|
104
|
+
credentials: Credentials,
|
|
105
|
+
data?: AuthUser,
|
|
106
|
+
deviceInfo?: DeviceInfo
|
|
107
|
+
): Promise<{
|
|
108
|
+
user: AuthUser;
|
|
109
|
+
session: AuthSession;
|
|
110
|
+
tokens: Tokens;
|
|
111
|
+
}> {
|
|
112
|
+
const userFindByEmail = await findUserByEmailProvider(
|
|
113
|
+
data?.email ?? "",
|
|
114
|
+
provider
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (userFindByEmail) {
|
|
118
|
+
throw new Error("User already registered");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const user = await createUser(
|
|
122
|
+
credentials.email,
|
|
123
|
+
provider,
|
|
124
|
+
data,
|
|
125
|
+
credentials.password
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (!user) {
|
|
129
|
+
throw new DatabasePackageError("Something went wrong");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const rawSession = await createUserSession(
|
|
133
|
+
user.id,
|
|
134
|
+
deviceInfo ?? {
|
|
135
|
+
ip: "",
|
|
136
|
+
browser: "",
|
|
137
|
+
os: "",
|
|
138
|
+
userAgent: "",
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const session: AuthSession = {
|
|
143
|
+
id: rawSession.id,
|
|
144
|
+
userId: rawSession.userId,
|
|
145
|
+
expiresAt: rawSession.expiresAt,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
user,
|
|
150
|
+
session,
|
|
151
|
+
tokens: { accessToken: "jwt-token" },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async signOut(_sessionId: string): Promise<void> {
|
|
156
|
+
throw new DatabasePackageError("signOut handled by NextAuth");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// -------------------
|
|
160
|
+
// Session
|
|
161
|
+
// -------------------
|
|
162
|
+
async getSession(_sessionId: string): Promise<AuthSession | null> {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async validateSession(_sessionId: string): Promise<boolean> {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async refreshSession(
|
|
171
|
+
_refreshToken: string
|
|
172
|
+
): Promise<{ session: AuthSession; tokens: Tokens }> {
|
|
173
|
+
throw new DatabasePackageError("RefreshSession handled by NextAuth");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -------------------
|
|
177
|
+
// Providers
|
|
178
|
+
// -------------------
|
|
179
|
+
async getOAuthUrl(
|
|
180
|
+
_provider: AUTHPROVIDER,
|
|
181
|
+
_redirectUri: string
|
|
182
|
+
): Promise<string> {
|
|
183
|
+
throw new DatabasePackageError("OAuth handled by NextAuth");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async handleOAuthCallback(
|
|
187
|
+
_provider: AUTHPROVIDER,
|
|
188
|
+
_code: string
|
|
189
|
+
): Promise<{ providerAccountId: string; profile: unknown }> {
|
|
190
|
+
throw new DatabasePackageError("OAuth callback handled by NextAuth");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// -------------------
|
|
194
|
+
// Connected Accounts
|
|
195
|
+
// -------------------
|
|
196
|
+
async linkAccount(
|
|
197
|
+
userId: string,
|
|
198
|
+
provider: AUTHPROVIDER,
|
|
199
|
+
data: ConnectedAccount
|
|
200
|
+
): Promise<ConnectedAccount> {
|
|
201
|
+
return linkConnectedAccount(userId, provider, data);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async unlinkAccount(userId: string, accountId: string): Promise<void> {
|
|
205
|
+
await unlinkConnectedAccount(userId, accountId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getLinkedAccounts(userId: string): Promise<ConnectedAccount[]> {
|
|
209
|
+
return getLinkedAccounts(userId);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AuthenticationError, } from "@plyaz/errors";
|
|
2
|
+
import { HTTP_STATUS } from "@plyaz/types";
|
|
3
|
+
export const API_BASE_URL =
|
|
4
|
+
globalThis.process.env.VITE_API_BASE_URL ?? "http://localhost:3000";
|
|
5
|
+
|
|
6
|
+
export async function apiFetch<T>(
|
|
7
|
+
path: string,
|
|
8
|
+
options: RequestInit = {}
|
|
9
|
+
): Promise<T> {
|
|
10
|
+
const res = await globalThis.fetch(`${API_BASE_URL}${path}`, {
|
|
11
|
+
credentials: "include", // IMPORTANT for session auth
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
...options.headers,
|
|
15
|
+
},
|
|
16
|
+
...options,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
let data = null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
data = await res.json();
|
|
24
|
+
} catch {
|
|
25
|
+
throw new AuthenticationError("AUTH_INVALID_CREDENTIALS", data?.message);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle empty responses (204 No Content)
|
|
32
|
+
if (res.status === HTTP_STATUS.NO_CONTENT) {
|
|
33
|
+
return null as T;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return res.json();
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface AuditEvent {
|
|
2
|
+
userId?: string;
|
|
3
|
+
action: string;
|
|
4
|
+
resource: string;
|
|
5
|
+
resourceId?: string;
|
|
6
|
+
ipAddress?: string;
|
|
7
|
+
userAgent?: string;
|
|
8
|
+
metadata?: Record<string, string>;
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AuditLogger {
|
|
13
|
+
private events: AuditEvent[] = [];
|
|
14
|
+
|
|
15
|
+
async log(event: Omit<AuditEvent, 'timestamp'>): Promise<void> {
|
|
16
|
+
const auditEvent: AuditEvent = {
|
|
17
|
+
...event,
|
|
18
|
+
timestamp: new Date()
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.events.push(auditEvent);
|
|
22
|
+
|
|
23
|
+
// This would write to audit.audit_logs table
|
|
24
|
+
globalThis.console.log('AUDIT:', JSON.stringify(auditEvent));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async logAuthEvent(action: string, userId?: string, metadata?: Record<string, string>): Promise<void> {
|
|
28
|
+
await this.log({
|
|
29
|
+
userId,
|
|
30
|
+
action,
|
|
31
|
+
resource: 'authentication',
|
|
32
|
+
metadata
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async logPermissionEvent(action: string, userId: string, resource: string, resourceId?: string): Promise<void> {
|
|
37
|
+
await this.log({
|
|
38
|
+
userId,
|
|
39
|
+
action,
|
|
40
|
+
resource,
|
|
41
|
+
resourceId
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getEvents(): AuditEvent[] {
|
|
46
|
+
return [...this.events];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clearEvents(): void {
|
|
50
|
+
this.events = [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useAuth } from '../hooks/useAuth';
|
|
4
|
+
|
|
5
|
+
interface ProtectedRouteProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
fallback?: ReactNode;
|
|
8
|
+
requiredRoles?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|
12
|
+
children,
|
|
13
|
+
fallback = <div>Please sign in to access this page</div>,
|
|
14
|
+
requiredRoles = []
|
|
15
|
+
// eslint-disable-next-line complexity
|
|
16
|
+
}) => {
|
|
17
|
+
const { isAuthenticated, user, isLoading } = useAuth();
|
|
18
|
+
|
|
19
|
+
if (isLoading) {
|
|
20
|
+
return <div>Loading...</div>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!isAuthenticated) {
|
|
24
|
+
return <>{fallback}</>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (requiredRoles.length > 0 && user) {
|
|
28
|
+
const userRoles = user.roles ?? [];
|
|
29
|
+
const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role));
|
|
30
|
+
|
|
31
|
+
if (!hasRequiredRole) {
|
|
32
|
+
return <div>Access denied. Required roles: {requiredRoles.join(', ')}</div>;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return <>{children}</>;
|
|
37
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useAuthStore } from "../store/auth.store";
|
|
2
|
+
import type {
|
|
3
|
+
AuthAdapterUser,
|
|
4
|
+
AuthCredentials,
|
|
5
|
+
AUTHPROVIDER,
|
|
6
|
+
AuthSession,
|
|
7
|
+
AuthUser,
|
|
8
|
+
Tokens,
|
|
9
|
+
|
|
10
|
+
ConnectedAccount,
|
|
11
|
+
} from "@plyaz/types";
|
|
12
|
+
|
|
13
|
+
export interface Permission {
|
|
14
|
+
resource: string;
|
|
15
|
+
action: string;
|
|
16
|
+
conditions: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AuthPermissions extends AuthUser {
|
|
20
|
+
permissions?: Permission[];
|
|
21
|
+
isActive: boolean;
|
|
22
|
+
isVerified: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseAuthReturn {
|
|
26
|
+
user: AuthPermissions | null;
|
|
27
|
+
isAuthenticated: boolean;
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
error: string | null;
|
|
30
|
+
signIn: (provider?: AUTHPROVIDER, credentials?: AuthCredentials) => Promise<{
|
|
31
|
+
user: AuthAdapterUser;
|
|
32
|
+
session: AuthSession;
|
|
33
|
+
tokens: Tokens;
|
|
34
|
+
}>;
|
|
35
|
+
signUp: (provider: AUTHPROVIDER, data?: unknown) => Promise<void>;
|
|
36
|
+
signOut: () => Promise<void>;
|
|
37
|
+
linkAccount: (
|
|
38
|
+
userId:string,provider:AUTHPROVIDER,data:ConnectedAccount
|
|
39
|
+
) => Promise<void | ConnectedAccount>;
|
|
40
|
+
unlinkAccount: (accountId: string) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useAuth(): UseAuthReturn {
|
|
44
|
+
const store = useAuthStore();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handles authentication actions with consistent loading and error states
|
|
48
|
+
*
|
|
49
|
+
* @param {Function} action - Async function to execute
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
const handleAuthAction = async <T,>(
|
|
55
|
+
action: () => Promise<T>
|
|
56
|
+
): Promise<T> => {
|
|
57
|
+
store.setLoading(true);
|
|
58
|
+
store.setError(null);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const result = await action();
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
catch (err: unknown) {
|
|
65
|
+
const errorMessage =
|
|
66
|
+
err instanceof Error ? err.message : "Authentication failed";
|
|
67
|
+
|
|
68
|
+
store.setError(errorMessage);
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
finally {
|
|
73
|
+
store.setLoading(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
user: store.user,
|
|
79
|
+
isAuthenticated: store.isAuthenticated,
|
|
80
|
+
isLoading: store.isLoading,
|
|
81
|
+
error: store.error,
|
|
82
|
+
signIn: async (
|
|
83
|
+
provider?: AUTHPROVIDER,
|
|
84
|
+
credentials?: AuthCredentials
|
|
85
|
+
): Promise<{
|
|
86
|
+
user: AuthAdapterUser;
|
|
87
|
+
session: AuthSession;
|
|
88
|
+
tokens: Tokens;
|
|
89
|
+
}> => {
|
|
90
|
+
return handleAuthAction(() => store.signIn(provider, credentials));
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
signUp: async (provider: AUTHPROVIDER, data?: unknown): Promise<void> => {
|
|
94
|
+
return handleAuthAction(() => store.signUp(provider, data));
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
signOut: async (): Promise<void> => {
|
|
98
|
+
return handleAuthAction(() => store.signOut());
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
linkAccount: async (
|
|
102
|
+
userId:string,provider:AUTHPROVIDER,data:ConnectedAccount
|
|
103
|
+
): Promise<void> => {
|
|
104
|
+
return handleAuthAction(async () => {
|
|
105
|
+
// Construct a ConnectedAccount object; adjust fields as needed
|
|
106
|
+
const connectedAccount: ConnectedAccount = {
|
|
107
|
+
|
|
108
|
+
...data,
|
|
109
|
+
userId,
|
|
110
|
+
provider,
|
|
111
|
+
providerType: "",
|
|
112
|
+
providerAccountId: "",
|
|
113
|
+
isPrimary: false,
|
|
114
|
+
isVerified: false,
|
|
115
|
+
isActive: false,
|
|
116
|
+
linkedAt: new Date(),
|
|
117
|
+
createdAt: new Date(),
|
|
118
|
+
updatedAt: new Date()
|
|
119
|
+
};
|
|
120
|
+
store.addConnectedAccount(connectedAccount);
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
unlinkAccount: async (accountId: string): Promise<void> => {
|
|
125
|
+
return handleAuthAction(async () => store.removeConnectedAccount(accountId));
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Connected accounts management hook
|
|
3
|
+
* @module @plyaz/auth/client/hooks/useConnectedAccounts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
import { useAuth } from './useAuth';
|
|
8
|
+
import type { AuthCredentials, ConnectedAccount } from '@plyaz/types';
|
|
9
|
+
|
|
10
|
+
export interface UseConnectedAccountsReturn {
|
|
11
|
+
accounts: ConnectedAccount[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
linkAccount: (provider: string, credentials: AuthCredentials) => Promise<void>;
|
|
14
|
+
unlinkAccount: (accountId: string) => Promise<void>;
|
|
15
|
+
setPrimaryAccount: (accountId: string) => Promise<void>;
|
|
16
|
+
refreshAccounts: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const useConnectedAccounts = (): UseConnectedAccountsReturn => {
|
|
20
|
+
const [accounts, setAccounts] = useState<ConnectedAccount[]>([]);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
22
|
+
const { user } = useAuth();
|
|
23
|
+
|
|
24
|
+
const linkAccount = async (provider: string, credentials:AuthCredentials):Promise<void> => {
|
|
25
|
+
if (!user) throw new Error('User not authenticated');
|
|
26
|
+
|
|
27
|
+
setIsLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
const response = await globalThis.fetch('/api/auth/accounts/link', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({ provider, credentials })
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!response.ok) throw new Error('Failed to link account');
|
|
36
|
+
|
|
37
|
+
const newAccount = await response.json() as ConnectedAccount;
|
|
38
|
+
setAccounts(prev => [...prev, newAccount]);
|
|
39
|
+
} finally {
|
|
40
|
+
setIsLoading(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const unlinkAccount = async (accountId: string):Promise<void> => {
|
|
45
|
+
setIsLoading(true);
|
|
46
|
+
try {
|
|
47
|
+
const response = await globalThis.fetch(`/api/auth/accounts/${accountId}/unlink`, {
|
|
48
|
+
method: 'DELETE'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) throw new Error('Failed to unlink account');
|
|
52
|
+
|
|
53
|
+
setAccounts(prev => prev.filter(acc => acc.id !== accountId));
|
|
54
|
+
} finally {
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const setPrimaryAccount = async (accountId: string):Promise<void> => {
|
|
60
|
+
setIsLoading(true);
|
|
61
|
+
try {
|
|
62
|
+
const response = await globalThis.fetch(`/api/auth/accounts/${accountId}/primary`, {
|
|
63
|
+
method: 'PUT'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) throw new Error('Failed to set primary account');
|
|
67
|
+
|
|
68
|
+
setAccounts(prev => prev.map(acc => ({
|
|
69
|
+
...acc,
|
|
70
|
+
isPrimary: acc.id === accountId
|
|
71
|
+
})));
|
|
72
|
+
} finally {
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const refreshAccounts = async ():Promise<void>=> {
|
|
78
|
+
if (!user) return;
|
|
79
|
+
|
|
80
|
+
setIsLoading(true);
|
|
81
|
+
try {
|
|
82
|
+
const response = await globalThis.fetch('/api/auth/accounts');
|
|
83
|
+
if (response.ok) {
|
|
84
|
+
const data = await response.json() as ConnectedAccount[];
|
|
85
|
+
setAccounts(data);
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
setIsLoading(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
void (async():Promise<void> => {
|
|
94
|
+
await refreshAccounts();
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
}, [user]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
accounts,
|
|
102
|
+
isLoading,
|
|
103
|
+
linkAccount,
|
|
104
|
+
unlinkAccount,
|
|
105
|
+
setPrimaryAccount,
|
|
106
|
+
refreshAccounts,
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Permissions management hook
|
|
3
|
+
* @module @plyaz/auth/client/hooks/usePermissions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Permission} from "./useAuth";
|
|
7
|
+
import { useAuth } from "./useAuth";
|
|
8
|
+
|
|
9
|
+
export interface UsePermissionsReturn {
|
|
10
|
+
permissions: Permission[];
|
|
11
|
+
hasPermission: (permission: Permission) => boolean;
|
|
12
|
+
hasAnyPermission: (permissions: Permission[]) => boolean;
|
|
13
|
+
hasAllPermissions: (permissions: Permission[]) => boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const usePermissions = (): UsePermissionsReturn => {
|
|
17
|
+
const { user } = useAuth();
|
|
18
|
+
|
|
19
|
+
const permissions = user?.permissions ?? [];
|
|
20
|
+
|
|
21
|
+
const hasPermission = (permission: Permission): boolean =>
|
|
22
|
+
permissions.includes(permission);
|
|
23
|
+
|
|
24
|
+
const hasAnyPermission = (perms: Permission[]): boolean =>
|
|
25
|
+
perms.some((perm) => permissions.includes(perm));
|
|
26
|
+
|
|
27
|
+
const hasAllPermissions = (perms: Permission[]): boolean =>
|
|
28
|
+
perms.every((perm) => permissions.includes(perm));
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
permissions,
|
|
32
|
+
hasPermission,
|
|
33
|
+
hasAnyPermission,
|
|
34
|
+
hasAllPermissions,
|
|
35
|
+
};
|
|
36
|
+
};
|