nexu-app 2.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/README.md +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthConfig,
|
|
3
|
+
AuthError,
|
|
4
|
+
AuthErrorCode,
|
|
5
|
+
AuthResponse,
|
|
6
|
+
AuthUser,
|
|
7
|
+
SignInCredentials,
|
|
8
|
+
SignUpCredentials,
|
|
9
|
+
UpdatePasswordRequest,
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
import { TokenManager } from './token';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auth API client for making authenticated requests
|
|
16
|
+
*/
|
|
17
|
+
export class AuthApiClient {
|
|
18
|
+
private baseUrl: string;
|
|
19
|
+
private headers: Record<string, string>;
|
|
20
|
+
private tokenManager: TokenManager;
|
|
21
|
+
private debug: boolean;
|
|
22
|
+
|
|
23
|
+
constructor(config: AuthConfig, tokenManager: TokenManager) {
|
|
24
|
+
this.baseUrl = config.apiBaseUrl.replace(/\/$/, '');
|
|
25
|
+
this.headers = config.headers || {};
|
|
26
|
+
this.tokenManager = tokenManager;
|
|
27
|
+
this.debug = config.debug || false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private log(...args: unknown[]): void {
|
|
31
|
+
if (this.debug) {
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.log('[Auth]', ...args);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
38
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
39
|
+
const token = this.tokenManager.getAccessToken();
|
|
40
|
+
|
|
41
|
+
const headers: Record<string, string> = {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
...this.headers,
|
|
44
|
+
...(options.headers as Record<string, string>),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (token) {
|
|
48
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.log(`${options.method || 'GET'} ${url}`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(url, {
|
|
55
|
+
...options,
|
|
56
|
+
headers,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const data = (await response.json().catch(() => ({}))) as T & {
|
|
60
|
+
message?: string;
|
|
61
|
+
code?: string;
|
|
62
|
+
details?: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw this.createError(response.status, data);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return data;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if ((error as AuthError).code) {
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
throw this.createError(0, {
|
|
76
|
+
message: error instanceof Error ? error.message : 'Network error',
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private createError(
|
|
82
|
+
status: number,
|
|
83
|
+
data: { message?: string; code?: string; details?: unknown }
|
|
84
|
+
): AuthError {
|
|
85
|
+
let code: AuthErrorCode = 'UNKNOWN_ERROR';
|
|
86
|
+
|
|
87
|
+
if (data.code) {
|
|
88
|
+
code = data.code as AuthErrorCode;
|
|
89
|
+
} else if (status === 401) {
|
|
90
|
+
code = 'UNAUTHORIZED';
|
|
91
|
+
} else if (status === 403) {
|
|
92
|
+
code = 'FORBIDDEN';
|
|
93
|
+
} else if (status === 404) {
|
|
94
|
+
code = 'USER_NOT_FOUND';
|
|
95
|
+
} else if (status === 409) {
|
|
96
|
+
code = 'USER_ALREADY_EXISTS';
|
|
97
|
+
} else if (status === 0) {
|
|
98
|
+
code = 'NETWORK_ERROR';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
code,
|
|
103
|
+
message: data.message || 'An error occurred',
|
|
104
|
+
details: data.details,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sign in with email and password
|
|
110
|
+
*/
|
|
111
|
+
async signIn(credentials: SignInCredentials): Promise<AuthResponse> {
|
|
112
|
+
return this.request<AuthResponse>('/signin', {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
body: JSON.stringify(credentials),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Sign up with email and password
|
|
120
|
+
*/
|
|
121
|
+
async signUp(credentials: SignUpCredentials): Promise<AuthResponse> {
|
|
122
|
+
return this.request<AuthResponse>('/signup', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
body: JSON.stringify(credentials),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sign out
|
|
130
|
+
*/
|
|
131
|
+
async signOut(): Promise<void> {
|
|
132
|
+
const refreshToken = this.tokenManager.getRefreshToken();
|
|
133
|
+
|
|
134
|
+
await this.request('/signout', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
body: JSON.stringify({ refreshToken }),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Refresh the access token
|
|
142
|
+
*/
|
|
143
|
+
async refreshToken(): Promise<AuthResponse> {
|
|
144
|
+
const refreshToken = this.tokenManager.getRefreshToken();
|
|
145
|
+
|
|
146
|
+
if (!refreshToken) {
|
|
147
|
+
throw this.createError(401, { message: 'No refresh token available' });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.request<AuthResponse>('/refresh', {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
body: JSON.stringify({ refreshToken }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the current user
|
|
158
|
+
*/
|
|
159
|
+
async getUser(): Promise<AuthUser> {
|
|
160
|
+
return this.request<AuthUser>('/me');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Update the current user
|
|
165
|
+
*/
|
|
166
|
+
async updateUser(data: Partial<AuthUser>): Promise<AuthUser> {
|
|
167
|
+
return this.request<AuthUser>('/me', {
|
|
168
|
+
method: 'PATCH',
|
|
169
|
+
body: JSON.stringify(data),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Request password reset
|
|
175
|
+
*/
|
|
176
|
+
async resetPassword(email: string): Promise<void> {
|
|
177
|
+
await this.request('/reset-password', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
body: JSON.stringify({ email }),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Update password
|
|
185
|
+
*/
|
|
186
|
+
async updatePassword(request: UpdatePasswordRequest): Promise<void> {
|
|
187
|
+
await this.request('/update-password', {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
body: JSON.stringify(request),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Verify email
|
|
195
|
+
*/
|
|
196
|
+
async verifyEmail(token: string): Promise<void> {
|
|
197
|
+
await this.request('/verify-email', {
|
|
198
|
+
method: 'POST',
|
|
199
|
+
body: JSON.stringify({ token }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resend verification email
|
|
205
|
+
*/
|
|
206
|
+
async resendVerification(): Promise<void> {
|
|
207
|
+
await this.request('/resend-verification', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Exchange OAuth code for tokens
|
|
214
|
+
*/
|
|
215
|
+
async exchangeOAuthCode(provider: string, code: string, state?: string): Promise<AuthResponse> {
|
|
216
|
+
return this.request<AuthResponse>(`/oauth/${provider}/callback`, {
|
|
217
|
+
method: 'POST',
|
|
218
|
+
body: JSON.stringify({ code, state }),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create an auth API client instance
|
|
225
|
+
*/
|
|
226
|
+
export function createAuthApiClient(config: AuthConfig, tokenManager: TokenManager): AuthApiClient {
|
|
227
|
+
return new AuthApiClient(config, tokenManager);
|
|
228
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { AuthProvider, AuthProviderConfig, OAuthProviderConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OAuth provider URLs
|
|
5
|
+
*/
|
|
6
|
+
const OAUTH_URLS: Record<string, { authUrl: string; defaultScope: string[] }> = {
|
|
7
|
+
google: {
|
|
8
|
+
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
9
|
+
defaultScope: ['openid', 'email', 'profile'],
|
|
10
|
+
},
|
|
11
|
+
github: {
|
|
12
|
+
authUrl: 'https://github.com/login/oauth/authorize',
|
|
13
|
+
defaultScope: ['read:user', 'user:email'],
|
|
14
|
+
},
|
|
15
|
+
facebook: {
|
|
16
|
+
authUrl: 'https://www.facebook.com/v18.0/dialog/oauth',
|
|
17
|
+
defaultScope: ['email', 'public_profile'],
|
|
18
|
+
},
|
|
19
|
+
apple: {
|
|
20
|
+
authUrl: 'https://appleid.apple.com/auth/authorize',
|
|
21
|
+
defaultScope: ['name', 'email'],
|
|
22
|
+
},
|
|
23
|
+
twitter: {
|
|
24
|
+
authUrl: 'https://twitter.com/i/oauth2/authorize',
|
|
25
|
+
defaultScope: ['users.read', 'tweet.read'],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generate a random state string for CSRF protection
|
|
31
|
+
*/
|
|
32
|
+
export function generateState(): string {
|
|
33
|
+
const array = new Uint8Array(32);
|
|
34
|
+
crypto.getRandomValues(array);
|
|
35
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate a code verifier for PKCE
|
|
40
|
+
*/
|
|
41
|
+
export function generateCodeVerifier(): string {
|
|
42
|
+
const array = new Uint8Array(32);
|
|
43
|
+
crypto.getRandomValues(array);
|
|
44
|
+
return base64UrlEncode(array);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate a code challenge from a code verifier
|
|
49
|
+
*/
|
|
50
|
+
export async function generateCodeChallenge(verifier: string): Promise<string> {
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
const data = encoder.encode(verifier);
|
|
53
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
54
|
+
return base64UrlEncode(new Uint8Array(hash));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Base64 URL encode
|
|
59
|
+
*/
|
|
60
|
+
function base64UrlEncode(buffer: Uint8Array): string {
|
|
61
|
+
let binary = '';
|
|
62
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
63
|
+
binary += String.fromCharCode(buffer[i]);
|
|
64
|
+
}
|
|
65
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Store OAuth state in session storage
|
|
70
|
+
*/
|
|
71
|
+
export function storeOAuthState(provider: string, state: string, codeVerifier?: string): void {
|
|
72
|
+
if (typeof window === 'undefined') return;
|
|
73
|
+
|
|
74
|
+
const data = { state, codeVerifier, timestamp: Date.now() };
|
|
75
|
+
sessionStorage.setItem(`oauth_state_${provider}`, JSON.stringify(data));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get stored OAuth state
|
|
80
|
+
*/
|
|
81
|
+
export function getStoredOAuthState(
|
|
82
|
+
provider: string
|
|
83
|
+
): { state: string; codeVerifier?: string } | null {
|
|
84
|
+
if (typeof window === 'undefined') return null;
|
|
85
|
+
|
|
86
|
+
const stored = sessionStorage.getItem(`oauth_state_${provider}`);
|
|
87
|
+
if (!stored) return null;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const data = JSON.parse(stored) as {
|
|
91
|
+
state: string;
|
|
92
|
+
codeVerifier?: string;
|
|
93
|
+
timestamp: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Check if state is older than 10 minutes
|
|
97
|
+
if (Date.now() - data.timestamp > 10 * 60 * 1000) {
|
|
98
|
+
sessionStorage.removeItem(`oauth_state_${provider}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { state: data.state, codeVerifier: data.codeVerifier };
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear stored OAuth state
|
|
110
|
+
*/
|
|
111
|
+
export function clearOAuthState(provider: string): void {
|
|
112
|
+
if (typeof window === 'undefined') return;
|
|
113
|
+
sessionStorage.removeItem(`oauth_state_${provider}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build OAuth authorization URL
|
|
118
|
+
*/
|
|
119
|
+
export function buildOAuthUrl(
|
|
120
|
+
provider: AuthProvider,
|
|
121
|
+
config: OAuthProviderConfig,
|
|
122
|
+
options?: {
|
|
123
|
+
state?: string;
|
|
124
|
+
codeChallenge?: string;
|
|
125
|
+
codeChallengeMethod?: string;
|
|
126
|
+
}
|
|
127
|
+
): string {
|
|
128
|
+
const providerConfig = OAUTH_URLS[provider];
|
|
129
|
+
if (!providerConfig) {
|
|
130
|
+
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const params = new URLSearchParams({
|
|
134
|
+
client_id: config.clientId,
|
|
135
|
+
redirect_uri: config.redirectUri || `${window.location.origin}/auth/callback/${provider}`,
|
|
136
|
+
response_type: 'code',
|
|
137
|
+
scope: (config.scope || providerConfig.defaultScope).join(' '),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
if (options?.state) {
|
|
141
|
+
params.set('state', options.state);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options?.codeChallenge) {
|
|
145
|
+
params.set('code_challenge', options.codeChallenge);
|
|
146
|
+
params.set('code_challenge_method', options.codeChallengeMethod || 'S256');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Provider-specific parameters
|
|
150
|
+
if (provider === 'google') {
|
|
151
|
+
params.set('access_type', 'offline');
|
|
152
|
+
params.set('prompt', 'consent');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return `${providerConfig.authUrl}?${params.toString()}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the OAuth config for a provider
|
|
160
|
+
*/
|
|
161
|
+
export function getProviderConfig(
|
|
162
|
+
provider: AuthProvider,
|
|
163
|
+
config?: AuthProviderConfig
|
|
164
|
+
): OAuthProviderConfig | null {
|
|
165
|
+
if (!config) return null;
|
|
166
|
+
|
|
167
|
+
switch (provider) {
|
|
168
|
+
case 'google':
|
|
169
|
+
return config.google || null;
|
|
170
|
+
case 'github':
|
|
171
|
+
return config.github || null;
|
|
172
|
+
case 'facebook':
|
|
173
|
+
return config.facebook || null;
|
|
174
|
+
case 'apple':
|
|
175
|
+
return config.apple || null;
|
|
176
|
+
case 'twitter':
|
|
177
|
+
return config.twitter || null;
|
|
178
|
+
default:
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Initiate OAuth flow
|
|
185
|
+
*/
|
|
186
|
+
export async function initiateOAuthFlow(
|
|
187
|
+
provider: AuthProvider,
|
|
188
|
+
providerConfig: OAuthProviderConfig,
|
|
189
|
+
usePKCE: boolean = true
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
const state = generateState();
|
|
192
|
+
let codeVerifier: string | undefined;
|
|
193
|
+
let codeChallenge: string | undefined;
|
|
194
|
+
|
|
195
|
+
if (usePKCE) {
|
|
196
|
+
codeVerifier = generateCodeVerifier();
|
|
197
|
+
codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
storeOAuthState(provider, state, codeVerifier);
|
|
201
|
+
|
|
202
|
+
const url = buildOAuthUrl(provider, providerConfig, {
|
|
203
|
+
state,
|
|
204
|
+
codeChallenge,
|
|
205
|
+
codeChallengeMethod: 'S256',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
window.location.href = url;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Parse OAuth callback URL parameters
|
|
213
|
+
*/
|
|
214
|
+
export function parseOAuthCallback(): {
|
|
215
|
+
code?: string;
|
|
216
|
+
state?: string;
|
|
217
|
+
error?: string;
|
|
218
|
+
errorDescription?: string;
|
|
219
|
+
} | null {
|
|
220
|
+
if (typeof window === 'undefined') return null;
|
|
221
|
+
|
|
222
|
+
const params = new URLSearchParams(window.location.search);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
code: params.get('code') || undefined,
|
|
226
|
+
state: params.get('state') || undefined,
|
|
227
|
+
error: params.get('error') || undefined,
|
|
228
|
+
errorDescription: params.get('error_description') || undefined,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import Cookies from 'js-cookie';
|
|
2
|
+
import { jwtDecode } from 'jwt-decode';
|
|
3
|
+
|
|
4
|
+
import type { TokenStorage, TokenPayload, AuthConfig } from '../types';
|
|
5
|
+
|
|
6
|
+
const ACCESS_TOKEN_KEY = 'auth_access_token';
|
|
7
|
+
const REFRESH_TOKEN_KEY = 'auth_refresh_token';
|
|
8
|
+
|
|
9
|
+
let memoryStorage: { accessToken?: string; refreshToken?: string } = {};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the token storage implementation based on config
|
|
13
|
+
*/
|
|
14
|
+
function getStorage(storage: TokenStorage) {
|
|
15
|
+
switch (storage) {
|
|
16
|
+
case 'localStorage':
|
|
17
|
+
return {
|
|
18
|
+
get: (key: string) => {
|
|
19
|
+
if (typeof window === 'undefined') return null;
|
|
20
|
+
return localStorage.getItem(key);
|
|
21
|
+
},
|
|
22
|
+
set: (key: string, value: string) => {
|
|
23
|
+
if (typeof window === 'undefined') return;
|
|
24
|
+
localStorage.setItem(key, value);
|
|
25
|
+
},
|
|
26
|
+
remove: (key: string) => {
|
|
27
|
+
if (typeof window === 'undefined') return;
|
|
28
|
+
localStorage.removeItem(key);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
case 'cookie':
|
|
32
|
+
return {
|
|
33
|
+
get: (key: string) => Cookies.get(key) || null,
|
|
34
|
+
set: (key: string, value: string, options?: Cookies.CookieAttributes) => {
|
|
35
|
+
Cookies.set(key, value, options);
|
|
36
|
+
},
|
|
37
|
+
remove: (key: string, options?: Cookies.CookieAttributes) => {
|
|
38
|
+
Cookies.remove(key, options);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
case 'memory':
|
|
42
|
+
return {
|
|
43
|
+
get: (key: string) => {
|
|
44
|
+
if (key === ACCESS_TOKEN_KEY) return memoryStorage.accessToken || null;
|
|
45
|
+
if (key === REFRESH_TOKEN_KEY) return memoryStorage.refreshToken || null;
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
48
|
+
set: (key: string, value: string) => {
|
|
49
|
+
if (key === ACCESS_TOKEN_KEY) memoryStorage.accessToken = value;
|
|
50
|
+
if (key === REFRESH_TOKEN_KEY) memoryStorage.refreshToken = value;
|
|
51
|
+
},
|
|
52
|
+
remove: (key: string) => {
|
|
53
|
+
if (key === ACCESS_TOKEN_KEY) delete memoryStorage.accessToken;
|
|
54
|
+
if (key === REFRESH_TOKEN_KEY) delete memoryStorage.refreshToken;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unknown storage type: ${storage as string}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Token manager class for handling token storage and validation
|
|
64
|
+
*/
|
|
65
|
+
export class TokenManager {
|
|
66
|
+
private storage: TokenStorage;
|
|
67
|
+
private cookieOptions?: AuthConfig['cookieOptions'];
|
|
68
|
+
|
|
69
|
+
constructor(config: Pick<AuthConfig, 'tokenStorage' | 'cookieOptions'>) {
|
|
70
|
+
this.storage = config.tokenStorage || 'cookie';
|
|
71
|
+
this.cookieOptions = config.cookieOptions;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the access token
|
|
76
|
+
*/
|
|
77
|
+
getAccessToken(): string | null {
|
|
78
|
+
return getStorage(this.storage).get(ACCESS_TOKEN_KEY);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get the refresh token
|
|
83
|
+
*/
|
|
84
|
+
getRefreshToken(): string | null {
|
|
85
|
+
return getStorage(this.storage).get(REFRESH_TOKEN_KEY);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Set the access token
|
|
90
|
+
*/
|
|
91
|
+
setAccessToken(token: string, expiresIn?: number): void {
|
|
92
|
+
const storage = getStorage(this.storage);
|
|
93
|
+
|
|
94
|
+
if (this.storage === 'cookie') {
|
|
95
|
+
const expires = expiresIn ? new Date(Date.now() + expiresIn * 1000) : undefined;
|
|
96
|
+
storage.set(ACCESS_TOKEN_KEY, token, {
|
|
97
|
+
expires,
|
|
98
|
+
secure: this.cookieOptions?.secure ?? true,
|
|
99
|
+
sameSite: this.cookieOptions?.sameSite ?? 'lax',
|
|
100
|
+
domain: this.cookieOptions?.domain,
|
|
101
|
+
path: this.cookieOptions?.path ?? '/',
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
storage.set(ACCESS_TOKEN_KEY, token);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set the refresh token
|
|
110
|
+
*/
|
|
111
|
+
setRefreshToken(token: string): void {
|
|
112
|
+
const storage = getStorage(this.storage);
|
|
113
|
+
|
|
114
|
+
if (this.storage === 'cookie') {
|
|
115
|
+
storage.set(REFRESH_TOKEN_KEY, token, {
|
|
116
|
+
expires: 30, // 30 days
|
|
117
|
+
secure: this.cookieOptions?.secure ?? true,
|
|
118
|
+
sameSite: this.cookieOptions?.sameSite ?? 'lax',
|
|
119
|
+
domain: this.cookieOptions?.domain,
|
|
120
|
+
path: this.cookieOptions?.path ?? '/',
|
|
121
|
+
httpOnly: false, // Can't set httpOnly from client-side
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
storage.set(REFRESH_TOKEN_KEY, token);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clear all tokens
|
|
130
|
+
*/
|
|
131
|
+
clearTokens(): void {
|
|
132
|
+
const storage = getStorage(this.storage);
|
|
133
|
+
|
|
134
|
+
if (this.storage === 'cookie') {
|
|
135
|
+
storage.remove(ACCESS_TOKEN_KEY, {
|
|
136
|
+
domain: this.cookieOptions?.domain,
|
|
137
|
+
path: this.cookieOptions?.path ?? '/',
|
|
138
|
+
});
|
|
139
|
+
storage.remove(REFRESH_TOKEN_KEY, {
|
|
140
|
+
domain: this.cookieOptions?.domain,
|
|
141
|
+
path: this.cookieOptions?.path ?? '/',
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
storage.remove(ACCESS_TOKEN_KEY);
|
|
145
|
+
storage.remove(REFRESH_TOKEN_KEY);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Also clear memory storage
|
|
149
|
+
memoryStorage = {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Decode the access token
|
|
154
|
+
*/
|
|
155
|
+
decodeToken(token?: string): TokenPayload | null {
|
|
156
|
+
const tokenToDecode = token || this.getAccessToken();
|
|
157
|
+
if (!tokenToDecode) return null;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
return jwtDecode<TokenPayload>(tokenToDecode);
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if the token is expired
|
|
168
|
+
*/
|
|
169
|
+
isTokenExpired(token?: string, threshold: number = 0): boolean {
|
|
170
|
+
const payload = this.decodeToken(token);
|
|
171
|
+
if (!payload || !payload.exp) return true;
|
|
172
|
+
|
|
173
|
+
const expiresAt = payload.exp * 1000;
|
|
174
|
+
return Date.now() >= expiresAt - threshold * 1000;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get token expiration time in milliseconds
|
|
179
|
+
*/
|
|
180
|
+
getTokenExpiration(token?: string): number | null {
|
|
181
|
+
const payload = this.decodeToken(token);
|
|
182
|
+
if (!payload || !payload.exp) return null;
|
|
183
|
+
return payload.exp * 1000;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if we should refresh the token
|
|
188
|
+
*/
|
|
189
|
+
shouldRefresh(threshold: number = 300): boolean {
|
|
190
|
+
const token = this.getAccessToken();
|
|
191
|
+
if (!token) return false;
|
|
192
|
+
|
|
193
|
+
return this.isTokenExpired(token, threshold);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a token manager instance
|
|
199
|
+
*/
|
|
200
|
+
export function createTokenManager(
|
|
201
|
+
config: Pick<AuthConfig, 'tokenStorage' | 'cookieOptions'>
|
|
202
|
+
): TokenManager {
|
|
203
|
+
return new TokenManager(config);
|
|
204
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@repo/config/typescript/base",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": "src",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"declarationMap": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*"],
|
|
13
|
+
"exclude": ["node_modules", "dist"]
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: [
|
|
5
|
+
'src/index.ts',
|
|
6
|
+
'src/hooks/index.ts',
|
|
7
|
+
'src/components/index.ts',
|
|
8
|
+
'src/providers/index.ts',
|
|
9
|
+
'src/next/index.ts',
|
|
10
|
+
],
|
|
11
|
+
format: ['cjs', 'esm'],
|
|
12
|
+
dts: true,
|
|
13
|
+
splitting: false,
|
|
14
|
+
sourcemap: true,
|
|
15
|
+
clean: true,
|
|
16
|
+
external: ['react', 'react-dom', 'next', 'next/server', 'next/headers'],
|
|
17
|
+
treeshake: true,
|
|
18
|
+
});
|