@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.
Files changed (113) hide show
  1. package/.github/pull_request_template.md +71 -0
  2. package/.github/workflows/deploy.yml +9 -0
  3. package/.github/workflows/publish.yml +14 -0
  4. package/.github/workflows/security.yml +20 -0
  5. package/README.md +89 -0
  6. package/commits.txt +5 -0
  7. package/dist/common/index.cjs +48 -0
  8. package/dist/common/index.cjs.map +1 -0
  9. package/dist/common/index.mjs +43 -0
  10. package/dist/common/index.mjs.map +1 -0
  11. package/dist/index.cjs +20411 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +5139 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +13 -0
  16. package/index.html +13 -0
  17. package/package.json +141 -0
  18. package/src/adapters/auth-adapter-factory.ts +26 -0
  19. package/src/adapters/auth-adapter.mapper.ts +53 -0
  20. package/src/adapters/base-auth.adapter.ts +119 -0
  21. package/src/adapters/clerk/clerk.adapter.ts +204 -0
  22. package/src/adapters/custom/custom.adapter.ts +119 -0
  23. package/src/adapters/index.ts +4 -0
  24. package/src/adapters/next-auth/authOptions.ts +81 -0
  25. package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
  26. package/src/api/client.ts +37 -0
  27. package/src/audit/audit.logger.ts +52 -0
  28. package/src/client/components/ProtectedRoute.tsx +37 -0
  29. package/src/client/hooks/useAuth.ts +128 -0
  30. package/src/client/hooks/useConnectedAccounts.ts +108 -0
  31. package/src/client/hooks/usePermissions.ts +36 -0
  32. package/src/client/hooks/useRBAC.ts +36 -0
  33. package/src/client/hooks/useSession.ts +18 -0
  34. package/src/client/providers/AuthProvider.tsx +104 -0
  35. package/src/client/store/auth.store.ts +306 -0
  36. package/src/client/utils/storage.ts +70 -0
  37. package/src/common/constants/oauth-providers.ts +49 -0
  38. package/src/common/errors/auth.errors.ts +64 -0
  39. package/src/common/errors/specific-auth-errors.ts +201 -0
  40. package/src/common/index.ts +19 -0
  41. package/src/common/regex/index.ts +27 -0
  42. package/src/common/types/auth.types.ts +641 -0
  43. package/src/common/types/index.ts +297 -0
  44. package/src/common/utils/index.ts +84 -0
  45. package/src/core/blacklist/token.blacklist.ts +60 -0
  46. package/src/core/index.ts +2 -0
  47. package/src/core/jwt/jwt.manager.ts +131 -0
  48. package/src/core/session/session.manager.ts +56 -0
  49. package/src/db/repositories/connected-account.repository.ts +415 -0
  50. package/src/db/repositories/role.repository.ts +519 -0
  51. package/src/db/repositories/session.repository.ts +308 -0
  52. package/src/db/repositories/user.repository.ts +320 -0
  53. package/src/flows/index.ts +2 -0
  54. package/src/flows/sign-in.flow.ts +106 -0
  55. package/src/flows/sign-up.flow.ts +121 -0
  56. package/src/index.ts +54 -0
  57. package/src/libs/clerk.helper.ts +36 -0
  58. package/src/libs/supabase.helper.ts +255 -0
  59. package/src/libs/supabaseClient.ts +6 -0
  60. package/src/providers/base/auth-provider.interface.ts +42 -0
  61. package/src/providers/base/index.ts +1 -0
  62. package/src/providers/index.ts +2 -0
  63. package/src/providers/oauth/facebook.provider.ts +97 -0
  64. package/src/providers/oauth/github.provider.ts +148 -0
  65. package/src/providers/oauth/google.provider.ts +126 -0
  66. package/src/providers/oauth/index.ts +3 -0
  67. package/src/rbac/dynamic-roles.ts +552 -0
  68. package/src/rbac/index.ts +4 -0
  69. package/src/rbac/permission-checker.ts +464 -0
  70. package/src/rbac/role-hierarchy.ts +545 -0
  71. package/src/rbac/role.manager.ts +75 -0
  72. package/src/security/csrf/csrf.protection.ts +37 -0
  73. package/src/security/index.ts +3 -0
  74. package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
  75. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
  76. package/src/security/rate-limiting/auth.module.ts +32 -0
  77. package/src/server/auth.module.ts +158 -0
  78. package/src/server/decorators/auth.decorator.ts +43 -0
  79. package/src/server/decorators/auth.decorators.ts +31 -0
  80. package/src/server/decorators/current-user.decorator.ts +49 -0
  81. package/src/server/decorators/permission.decorator.ts +49 -0
  82. package/src/server/guards/auth.guard.ts +56 -0
  83. package/src/server/guards/custom-throttler.guard.ts +46 -0
  84. package/src/server/guards/permissions.guard.ts +115 -0
  85. package/src/server/guards/roles.guard.ts +31 -0
  86. package/src/server/middleware/auth.middleware.ts +46 -0
  87. package/src/server/middleware/index.ts +2 -0
  88. package/src/server/middleware/middleware.ts +11 -0
  89. package/src/server/middleware/session.middleware.ts +255 -0
  90. package/src/server/services/account.service.ts +269 -0
  91. package/src/server/services/auth.service.ts +79 -0
  92. package/src/server/services/brute-force.service.ts +98 -0
  93. package/src/server/services/index.ts +15 -0
  94. package/src/server/services/rate-limiter.service.ts +60 -0
  95. package/src/server/services/session.service.ts +287 -0
  96. package/src/server/services/token.service.ts +262 -0
  97. package/src/session/cookie-store.ts +255 -0
  98. package/src/session/enhanced-session-manager.ts +406 -0
  99. package/src/session/index.ts +14 -0
  100. package/src/session/memory-store.ts +320 -0
  101. package/src/session/redis-store.ts +443 -0
  102. package/src/strategies/oauth.strategy.ts +128 -0
  103. package/src/strategies/traditional-auth.strategy.ts +116 -0
  104. package/src/tokens/index.ts +4 -0
  105. package/src/tokens/refresh-token-manager.ts +448 -0
  106. package/src/tokens/token-validator.ts +311 -0
  107. package/tsconfig.build.json +28 -0
  108. package/tsconfig.json +38 -0
  109. package/tsup.config.mjs +28 -0
  110. package/vitest.config.mjs +16 -0
  111. package/vitest.setup.d.ts +2 -0
  112. package/vitest.setup.d.ts.map +1 -0
  113. 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
+ };