hightjs 0.1.1 → 0.2.1

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.
@@ -0,0 +1,231 @@
1
+ import type {AuthProviderClass, AuthRoute, User} from '../types';
2
+ import {HightJSRequest, HightJSResponse} from '../../api/http';
3
+
4
+ export interface DiscordConfig {
5
+ id?: string;
6
+ name?: string;
7
+ clientId: string;
8
+ clientSecret: string;
9
+ callbackUrl?: string;
10
+ successUrl?: string;
11
+ // Escopos OAuth, padrão: ['identify', 'email']
12
+ scope?: string[];
13
+ }
14
+
15
+ /**
16
+ * Provider para autenticação com Discord OAuth2
17
+ *
18
+ * Este provider permite autenticação usando Discord OAuth2.
19
+ * Automaticamente gerencia o fluxo OAuth completo e rotas necessárias.
20
+ *
21
+ * Exemplo de uso:
22
+ * ```typescript
23
+ * new DiscordProvider({
24
+ * clientId: process.env.DISCORD_CLIENT_ID!,
25
+ * clientSecret: process.env.DISCORD_CLIENT_SECRET!,
26
+ * callbackUrl: "http://localhost:3000/api/auth/callback/discord"
27
+ * })
28
+ * ```
29
+ *
30
+ * Fluxo de autenticação:
31
+ * 1. GET /api/auth/signin/discord - Gera URL e redireciona para Discord
32
+ * 2. Discord redireciona para /api/auth/callback/discord com código
33
+ * 3. Provider troca código por token e busca dados do usuário
34
+ * 4. Retorna objeto User com dados do Discord
35
+ */
36
+ export class DiscordProvider implements AuthProviderClass {
37
+ public readonly id: string;
38
+ public readonly name: string;
39
+ public readonly type: string = 'discord';
40
+
41
+ private config: DiscordConfig;
42
+ private readonly defaultScope = ['identify', 'email'];
43
+
44
+ constructor(config: DiscordConfig) {
45
+ this.config = config;
46
+ this.id = config.id || 'discord';
47
+ this.name = config.name || 'Discord';
48
+ }
49
+
50
+ /**
51
+ * Método para gerar URL OAuth (usado pelo handleSignIn)
52
+ */
53
+ handleOauth(credentials: Record<string, string> = {}): string {
54
+ return this.getAuthorizationUrl();
55
+ }
56
+
57
+ /**
58
+ * Método principal - agora redireciona para OAuth ou processa callback
59
+ */
60
+ async handleSignIn(credentials: Record<string, string>): Promise<User | string | null> {
61
+ // Se tem código, é callback - processa autenticação
62
+ if (credentials.code) {
63
+ return await this.processOAuthCallback(credentials);
64
+ }
65
+
66
+ // Se não tem código, é início do OAuth - retorna URL
67
+ return this.handleOauth(credentials);
68
+ }
69
+
70
+ /**
71
+ * Processa o callback OAuth (código → usuário)
72
+ */
73
+ private async processOAuthCallback(credentials: Record<string, string>): Promise<User | null> {
74
+ try {
75
+ const { code } = credentials;
76
+ if (!code) {
77
+ throw new Error('Authorization code not provided');
78
+ }
79
+
80
+
81
+ // Troca o código por access token
82
+ const tokenResponse = await fetch('https://discord.com/api/oauth2/token', {
83
+ method: 'POST',
84
+ headers: {
85
+ 'Content-Type': 'application/x-www-form-urlencoded',
86
+ },
87
+ body: new URLSearchParams({
88
+ client_id: this.config.clientId,
89
+ client_secret: this.config.clientSecret,
90
+ grant_type: 'authorization_code',
91
+ code,
92
+ redirect_uri: this.config.callbackUrl || '',
93
+ }),
94
+ });
95
+
96
+ if (!tokenResponse.ok) {
97
+ const error = await tokenResponse.text();
98
+ // O erro original "Invalid \"code\" in request." acontece aqui.
99
+ throw new Error(`Failed to exchange code for token: ${error}`);
100
+ }
101
+
102
+ const tokens = await tokenResponse.json();
103
+
104
+ // Busca dados do usuário
105
+ const userResponse = await fetch('https://discord.com/api/users/@me', {
106
+ headers: {
107
+ 'Authorization': `Bearer ${tokens.access_token}`,
108
+ },
109
+ });
110
+
111
+ if (!userResponse.ok) {
112
+ throw new Error('Failed to fetch user data');
113
+ }
114
+
115
+ const discordUser = await userResponse.json();
116
+
117
+ // Retorna objeto User padronizado
118
+ return {
119
+ id: discordUser.id,
120
+ name: discordUser.global_name || discordUser.username,
121
+ email: discordUser.email,
122
+ image: discordUser.avatar
123
+ ? `https://cdn.discordapp.com/avatars/${discordUser.id}/${discordUser.avatar}.png`
124
+ : null,
125
+ username: discordUser.username,
126
+ discriminator: discordUser.discriminator,
127
+ provider: this.id,
128
+ providerId: discordUser.id,
129
+ accessToken: tokens.access_token,
130
+ refreshToken: tokens.refresh_token
131
+ };
132
+
133
+ } catch (error) {
134
+ console.error(`[${this.id} Provider] Error during OAuth callback:`, error);
135
+ return null;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Método opcional para logout
141
+ */
142
+ async handleSignOut?(): Promise<void> {
143
+ // Discord OAuth não precisa de logout especial
144
+ // O token será invalidado pelo tempo de vida
145
+ console.log(`[${this.id} Provider] User signed out`);
146
+ }
147
+
148
+ /**
149
+ * Rotas adicionais específicas do Discord OAuth
150
+ */
151
+ public additionalRoutes: AuthRoute[] = [
152
+ // Rota de callback do Discord
153
+ {
154
+ method: 'GET',
155
+ path: '/api/auth/callback/discord',
156
+ handler: async (req: HightJSRequest, params: any) => {
157
+ const url = new URL(req.url || '', 'http://localhost');
158
+ const code = url.searchParams.get('code');
159
+
160
+ if (!code) {
161
+ return HightJSResponse.json({ error: 'Authorization code not provided' }, { status: 400 });
162
+ }
163
+
164
+ try {
165
+ // CORREÇÃO: O fluxo correto é delegar o 'code' para o endpoint de signin
166
+ // principal, que processará o código uma única vez. A implementação anterior
167
+ // usava o código duas vezes, causando o erro 'invalid_grant'.
168
+ const authResponse = await fetch(`${req.headers.origin || 'http://localhost:3000'}/api/auth/signin`, {
169
+ method: 'POST',
170
+ headers: {
171
+ 'Content-Type': 'application/json',
172
+ },
173
+ body: JSON.stringify({
174
+ provider: this.id,
175
+ code,
176
+ })
177
+ });
178
+
179
+ if (authResponse.ok) {
180
+ // Propaga o cookie de sessão retornado pelo endpoint de signin
181
+ // e redireciona o usuário para a página de sucesso.
182
+ const setCookieHeader = authResponse.headers.get('set-cookie');
183
+ if(this.config.successUrl) {
184
+ return HightJSResponse
185
+ .redirect(this.config.successUrl)
186
+ .header('Set-Cookie', setCookieHeader || '');
187
+ }
188
+ return HightJSResponse.json({ success: true })
189
+ .header('Set-Cookie', setCookieHeader || '');
190
+ } else {
191
+ const errorText = await authResponse.text();
192
+ console.error(`[${this.id} Provider] Session creation failed during callback. Status: ${authResponse.status}, Body: ${errorText}`);
193
+ return HightJSResponse.json({ error: 'Session creation failed' }, { status: 500 });
194
+ }
195
+
196
+ } catch (error) {
197
+ console.error(`[${this.id} Provider] Callback handler fetch error:`, error);
198
+ return HightJSResponse.json({ error: 'Internal server error' }, { status: 500 });
199
+ }
200
+ }
201
+ }
202
+ ];
203
+
204
+ /**
205
+ * Gera URL de autorização do Discord
206
+ */
207
+ getAuthorizationUrl(): string {
208
+ const params = new URLSearchParams({
209
+ client_id: this.config.clientId,
210
+ redirect_uri: this.config.callbackUrl || '',
211
+ response_type: 'code',
212
+ scope: (this.config.scope || this.defaultScope).join(' ')
213
+ });
214
+
215
+ return `https://discord.com/api/oauth2/authorize?${params.toString()}`;
216
+ }
217
+
218
+ /**
219
+ * Retorna configuração pública do provider
220
+ */
221
+ getConfig(): any {
222
+ return {
223
+ id: this.id,
224
+ name: this.name,
225
+ type: this.type,
226
+ clientId: this.config.clientId, // Público
227
+ scope: this.config.scope || this.defaultScope,
228
+ callbackUrl: this.config.callbackUrl
229
+ };
230
+ }
231
+ }
@@ -0,0 +1,4 @@
1
+ // Exportações dos providers
2
+ export * from './credentials';
3
+ export * from './discord';
4
+
@@ -1,13 +1,4 @@
1
- import type { AuthProvider, CredentialsConfig } from './types';
1
+ // Exportações dos providers
2
+ export { CredentialsProvider } from './providers/credentials';
3
+ export { DiscordProvider } from './providers/discord';
2
4
 
3
- /**
4
- * Provider para autenticação com credenciais (email/senha)
5
- */
6
- export function CredentialsProvider(config: CredentialsConfig): AuthProvider {
7
- return {
8
- id: config.id || 'credentials',
9
- name: config.name || 'Credentials',
10
- type: 'credentials',
11
- authorize: config.authorize
12
- };
13
- }
@@ -1,6 +1,6 @@
1
1
  import React, { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
2
2
  import type { Session, SessionContextType, SignInOptions, SignInResult, User } from './types';
3
- import {router} from "../client";
3
+ import { router } from "../client/clientRouter";
4
4
 
5
5
  const SessionContext = createContext<SessionContextType | undefined>(undefined);
6
6
 
@@ -75,23 +75,31 @@ export function SessionProvider({
75
75
  const data = await response.json();
76
76
 
77
77
  if (response.ok && data.success) {
78
- // Atualiza a sessão após login bem-sucedido
78
+ // Se é OAuth, redireciona para URL fornecida
79
+ if (data.type === 'oauth' && data.redirectUrl) {
80
+ if (redirect && typeof window !== 'undefined') {
81
+ window.location.href = data.redirectUrl;
82
+ }
83
+
84
+ return {
85
+ ok: true,
86
+ status: 200,
87
+ url: data.redirectUrl
88
+ };
89
+ }
79
90
 
80
- if (redirect && typeof window !== 'undefined') {
81
- try {
82
- router.push(callbackUrl || '/');
83
- } catch (e) {
91
+ // Se é sessão (credentials), redireciona para callbackUrl
92
+ if (data.type === 'session') {
93
+ if (redirect && typeof window !== 'undefined') {
84
94
  window.location.href = callbackUrl || '/';
85
95
  }
86
96
 
97
+ return {
98
+ ok: true,
99
+ status: 200,
100
+ url: callbackUrl || '/'
101
+ };
87
102
  }
88
- await fetchSession();
89
-
90
- return {
91
- ok: true,
92
- status: 200,
93
- url: callbackUrl || '/'
94
- };
95
103
  } else {
96
104
  return {
97
105
  error: data.error || 'Authentication failed',
@@ -13,12 +13,28 @@ export function createAuthRoutes(config: AuthConfig) {
13
13
  * Uso: /api/auth/[...value].ts
14
14
  */
15
15
  return {
16
- pattern: '/api/auth/[value]',
16
+ pattern: '/api/auth/[...value]',
17
17
 
18
18
  async GET(req: HightJSRequest, params: { [key: string]: string }) {
19
+
19
20
  const path = params["value"];
20
21
  const route = Array.isArray(path) ? path.join('/') : path || '';
21
22
 
23
+ // Verifica rotas adicionais dos providers primeiro
24
+ const additionalRoutes = auth.getAllAdditionalRoutes();
25
+ for (const { provider, route: additionalRoute } of additionalRoutes) {
26
+
27
+ if (additionalRoute.method === 'GET' && additionalRoute.path.includes(route)) {
28
+ try {
29
+ return await additionalRoute.handler(req, params);
30
+ } catch (error) {
31
+ console.error(`[${provider} Provider] Error in additional route:`, error);
32
+ return HightJSResponse.json({ error: 'Provider route error' }, { status: 500 });
33
+ }
34
+ }
35
+ }
36
+
37
+ // Rotas padrão do sistema
22
38
  switch (route) {
23
39
  case 'session':
24
40
  return await handleSession(req, auth);
@@ -27,7 +43,7 @@ export function createAuthRoutes(config: AuthConfig) {
27
43
  return await handleCsrf(req);
28
44
 
29
45
  case 'providers':
30
- return await handleProviders(config);
46
+ return await handleProviders(auth);
31
47
 
32
48
  default:
33
49
  return HightJSResponse.json({ error: 'Route not found' }, { status: 404 });
@@ -38,6 +54,20 @@ export function createAuthRoutes(config: AuthConfig) {
38
54
  const path = params["value"];
39
55
  const route = Array.isArray(path) ? path.join('/') : path || '';
40
56
 
57
+ // Verifica rotas adicionais dos providers primeiro
58
+ const additionalRoutes = auth.getAllAdditionalRoutes();
59
+ for (const { provider, route: additionalRoute } of additionalRoutes) {
60
+ if (additionalRoute.method === 'POST' && additionalRoute.path.includes(route)) {
61
+ try {
62
+ return await additionalRoute.handler(req, params);
63
+ } catch (error) {
64
+ console.error(`[${provider} Provider] Error in additional route:`, error);
65
+ return HightJSResponse.json({ error: 'Provider route error' }, { status: 500 });
66
+ }
67
+ }
68
+ }
69
+
70
+ // Rotas padrão do sistema
41
71
  switch (route) {
42
72
  case 'signin':
43
73
  return await handleSignIn(req, auth);
@@ -50,7 +80,6 @@ export function createAuthRoutes(config: AuthConfig) {
50
80
  }
51
81
  },
52
82
 
53
-
54
83
  // Instância do auth para uso manual
55
84
  auth
56
85
  };
@@ -59,7 +88,7 @@ export function createAuthRoutes(config: AuthConfig) {
59
88
  /**
60
89
  * Handler para GET /api/auth/session
61
90
  */
62
- async function handleSession(req: HightJSRequest, auth: any) {
91
+ async function handleSession(req: HightJSRequest, auth: HWebAuth) {
63
92
  const session = await auth.getSession(req);
64
93
 
65
94
  if (!session) {
@@ -83,14 +112,8 @@ async function handleCsrf(req: HightJSRequest) {
83
112
  /**
84
113
  * Handler para GET /api/auth/providers
85
114
  */
86
- async function handleProviders(config: AuthConfig) {
87
- const providers = config.providers
88
- .filter(p => p.type === 'credentials') // Apenas credentials
89
- .map(p => ({
90
- id: p.id,
91
- name: p.name,
92
- type: p.type
93
- }));
115
+ async function handleProviders(auth: HWebAuth) {
116
+ const providers = auth.getProviders();
94
117
 
95
118
  return HightJSResponse.json({ providers });
96
119
  }
@@ -98,11 +121,10 @@ async function handleProviders(config: AuthConfig) {
98
121
  /**
99
122
  * Handler para POST /api/auth/signin
100
123
  */
101
- async function handleSignIn(req: HightJSRequest, auth: any) {
124
+ async function handleSignIn(req: HightJSRequest, auth: HWebAuth) {
102
125
  try {
103
126
  const { provider = 'credentials', ...credentials } = await req.json();
104
127
 
105
- // Apenas credentials agora
106
128
  const result = await auth.signIn(provider, credentials);
107
129
 
108
130
  if (!result) {
@@ -112,11 +134,23 @@ async function handleSignIn(req: HightJSRequest, auth: any) {
112
134
  );
113
135
  }
114
136
 
137
+ // Se tem redirectUrl, é OAuth - retorna URL para redirecionamento
138
+ if ('redirectUrl' in result) {
139
+ return HightJSResponse.json({
140
+ success: true,
141
+ redirectUrl: result.redirectUrl,
142
+ type: 'oauth'
143
+ });
144
+ }
145
+
146
+ // Se tem session, é credentials - retorna sessão
115
147
  return auth.createAuthResponse(result.token, {
116
148
  success: true,
117
- user: result.session.user
149
+ user: result.session.user,
150
+ type: 'session'
118
151
  });
119
152
  } catch (error) {
153
+ console.error('[hweb-auth] Erro no handleSignIn:', error);
120
154
  return HightJSResponse.json(
121
155
  { error: 'Authentication failed' },
122
156
  { status: 500 }
@@ -127,7 +161,6 @@ async function handleSignIn(req: HightJSRequest, auth: any) {
127
161
  /**
128
162
  * Handler para POST /api/auth/signout
129
163
  */
130
- async function handleSignOut(req: HightJSRequest, auth: any) {
131
- return auth.signOut();
164
+ async function handleSignOut(req: HightJSRequest, auth: HWebAuth) {
165
+ return await auth.signOut(req);
132
166
  }
133
-
package/src/auth/types.ts CHANGED
@@ -7,8 +7,55 @@ export interface Session {
7
7
  accessToken?: string;
8
8
  }
9
9
 
10
+ // Client-side types
11
+ export interface SignInOptions {
12
+ redirect?: boolean;
13
+ callbackUrl?: string;
14
+ [key: string]: any;
15
+ }
16
+
17
+ export interface SignInResult {
18
+ error?: string;
19
+ status?: number;
20
+ ok?: boolean;
21
+ url?: string;
22
+ }
23
+
24
+ export interface SessionContextType {
25
+ data: Session | null;
26
+ status: 'loading' | 'authenticated' | 'unauthenticated';
27
+ signIn: (provider?: string, options?: SignInOptions) => Promise<SignInResult | undefined>;
28
+ signOut: (options?: { callbackUrl?: string }) => Promise<void>;
29
+ update: () => Promise<Session | null>;
30
+ }
31
+
32
+ export interface AuthRoute {
33
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
34
+ path: string;
35
+ handler: (req: any, params: any) => Promise<any>;
36
+ }
37
+
38
+ export interface AuthProviderClass {
39
+ id: string;
40
+ name: string;
41
+ type: string;
42
+
43
+ // Para providers OAuth - retorna URL de redirecionamento
44
+ handleOauth?(credentials: Record<string, string>): Promise<string> | string;
45
+
46
+ // Métodos principais
47
+ handleSignIn(credentials: Record<string, string>): Promise<User | string | null>;
48
+ handleSignOut?(): Promise<void>;
49
+
50
+ // Rotas adicionais que o provider pode ter
51
+ additionalRoutes?: AuthRoute[];
52
+
53
+ // Configurações específicas do provider
54
+ getConfig?(): any;
55
+ }
56
+
10
57
  export interface AuthConfig {
11
- providers: AuthProvider[];
58
+ providers: AuthProviderClass[];
12
59
  pages?: {
13
60
  signIn?: string;
14
61
  signOut?: string;
@@ -28,6 +75,7 @@ export interface AuthConfig {
28
75
  debug?: boolean;
29
76
  }
30
77
 
78
+ // Interface legada para compatibilidade
31
79
  export interface AuthProvider {
32
80
  id: string;
33
81
  name: string;
@@ -35,27 +83,6 @@ export interface AuthProvider {
35
83
  authorize?: (credentials: Record<string, string>) => Promise<User | null> | User | null;
36
84
  }
37
85
 
38
- export interface SignInOptions {
39
- redirect?: boolean;
40
- callbackUrl?: string;
41
- [key: string]: any;
42
- }
43
-
44
- export interface SignInResult {
45
- error?: string;
46
- status?: number;
47
- ok?: boolean;
48
- url?: string;
49
- }
50
-
51
- export interface SessionContextType {
52
- data: Session | null;
53
- status: 'loading' | 'authenticated' | 'unauthenticated';
54
- signIn: (provider?: string, options?: SignInOptions) => Promise<SignInResult | undefined>;
55
- signOut: (options?: { callbackUrl?: string }) => Promise<void>;
56
- update: () => Promise<Session | null>;
57
- }
58
-
59
86
  // Provider para credenciais
60
87
  export interface CredentialsConfig {
61
88
  id?: string;
@@ -67,7 +94,3 @@ export interface CredentialsConfig {
67
94
  }>;
68
95
  authorize: (credentials: Record<string, string>) => Promise<User | null> | User | null;
69
96
  }
70
-
71
-
72
-
73
-
package/src/router.ts CHANGED
@@ -341,8 +341,16 @@ export function findMatchingBackendRoute(pathname: string, method: string) {
341
341
  for (const route of allBackendRoutes) {
342
342
  // Verifica se a rota tem um handler para o método HTTP atual
343
343
  if (!route.pattern || !route[method.toUpperCase() as keyof BackendRouteConfig]) continue;
344
+ const regexPattern = route.pattern
345
+ // [[...param]] → opcional catch-all
346
+ .replace(/\[\[\.\.\.(\w+)\]\]/g, '(?<$1>.+)?')
347
+ // [...param] → obrigatório catch-all
348
+ .replace(/\[\.\.\.(\w+)\]/g, '(?<$1>.+)')
349
+ // [[param]] → segmento opcional
350
+ .replace(/\[\[(\w+)\]\]/g, '(?<$1>[^/]+)?')
351
+ // [param] → segmento obrigatório
352
+ .replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
344
353
 
345
- const regexPattern = route.pattern.replace(/\[(\w+)\]/g, '(?<$1>[^/]+)');
346
354
  const regex = new RegExp(`^${regexPattern}/?$`);
347
355
  const match = pathname.match(regex);
348
356