@brightweblabs/core-auth 0.1.1 → 0.2.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/package.json +7 -8
- package/src/client.ts +0 -58
- package/src/server.ts +92 -25
- package/src/shared.ts +83 -0
- package/src/index.ts +0 -1
package/package.json
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brightweblabs/core-auth",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
5
|
-
"main": "./src/index.ts",
|
|
6
|
-
"types": "./src/index.ts",
|
|
4
|
+
"version": "0.2.0",
|
|
7
5
|
"files": [
|
|
8
6
|
"src"
|
|
9
7
|
],
|
|
@@ -16,15 +14,16 @@
|
|
|
16
14
|
"access": "public"
|
|
17
15
|
},
|
|
18
16
|
"exports": {
|
|
19
|
-
".": "./src/index.ts",
|
|
20
17
|
"./client": "./src/client.ts",
|
|
21
|
-
"./server": "./src/server.ts"
|
|
18
|
+
"./server": "./src/server.ts",
|
|
19
|
+
"./shared": "./src/shared.ts"
|
|
22
20
|
},
|
|
23
21
|
"dependencies": {
|
|
24
|
-
"
|
|
25
|
-
"@supabase/supabase-js": "^2.89.0"
|
|
22
|
+
"next": "16.1.1",
|
|
23
|
+
"@supabase/supabase-js": "^2.89.0",
|
|
24
|
+
"@brightweblabs/infra": "0.1.1"
|
|
26
25
|
},
|
|
27
26
|
"peerDependencies": {
|
|
28
27
|
"react": "^19.0.0"
|
|
29
28
|
}
|
|
30
|
-
}
|
|
29
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
|
|
5
|
-
export const AUTH_RESEND_COOLDOWN_SECONDS = 60;
|
|
6
|
-
|
|
7
|
-
export interface PasswordValidationResult {
|
|
8
|
-
isValid: boolean;
|
|
9
|
-
errors: string[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
interface CooldownTimerResult {
|
|
13
6
|
remaining: number;
|
|
14
7
|
isCoolingDown: boolean;
|
|
@@ -16,57 +9,6 @@ interface CooldownTimerResult {
|
|
|
16
9
|
reset: () => void;
|
|
17
10
|
}
|
|
18
11
|
|
|
19
|
-
export function getAuthBaseUrl(): string {
|
|
20
|
-
const configuredUrl = process.env.NEXT_PUBLIC_APP_URL?.trim();
|
|
21
|
-
if (!configuredUrl) {
|
|
22
|
-
throw new Error("Falta NEXT_PUBLIC_APP_URL para redirecionamentos de email de autenticação.");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
return new URL(configuredUrl).toString().replace(/\/$/, "");
|
|
27
|
-
} catch {
|
|
28
|
-
throw new Error("NEXT_PUBLIC_APP_URL inválido. Esperado URL absoluto (ex.: https://exemplo.com).");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function buildSignupCallbackUrl(): string {
|
|
33
|
-
return new URL("auth/callback", `${getAuthBaseUrl()}/`).toString();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function buildResetPasswordRedirectUrl(): string {
|
|
37
|
-
return `${getAuthBaseUrl()}/reset-password`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function validatePassword(password: string): PasswordValidationResult {
|
|
41
|
-
const errors: string[] = [];
|
|
42
|
-
|
|
43
|
-
if (password.length < 8) {
|
|
44
|
-
errors.push("A palavra-passe deve ter pelo menos 8 caracteres");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!/[A-Z]/.test(password)) {
|
|
48
|
-
errors.push("A palavra-passe deve conter pelo menos uma letra maiúscula");
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (!/[a-z]/.test(password)) {
|
|
52
|
-
errors.push("A palavra-passe deve conter pelo menos uma letra minúscula");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!/[0-9]/.test(password)) {
|
|
56
|
-
errors.push("A palavra-passe deve conter pelo menos um número");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
isValid: errors.length === 0,
|
|
61
|
-
errors,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function validateEmail(email: string): boolean {
|
|
66
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
67
|
-
return emailRegex.test(email.trim().toLowerCase());
|
|
68
|
-
}
|
|
69
|
-
|
|
70
12
|
export function useCooldownTimer(durationSeconds: number): CooldownTimerResult {
|
|
71
13
|
const [remaining, setRemaining] = useState(0);
|
|
72
14
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
package/src/server.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { User } from "@supabase/supabase-js";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
2
3
|
import { createServerSupabase } from "@brightweblabs/infra/server";
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
import {
|
|
5
|
+
canAccessInsightsCms,
|
|
6
|
+
normalizeGlobalRole,
|
|
7
|
+
resolvePostLoginPath,
|
|
8
|
+
type GlobalRole,
|
|
9
|
+
} from "./shared";
|
|
5
10
|
|
|
6
11
|
type Profile = {
|
|
7
12
|
id: string;
|
|
@@ -29,31 +34,21 @@ type ServerRoleAccess =
|
|
|
29
34
|
error: string;
|
|
30
35
|
};
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
type ServerUserAccess =
|
|
38
|
+
| {
|
|
39
|
+
ok: true;
|
|
40
|
+
supabase: Awaited<ReturnType<typeof createServerSupabase>>;
|
|
41
|
+
user: User;
|
|
42
|
+
profileId: string;
|
|
43
|
+
role: GlobalRole | null;
|
|
39
44
|
}
|
|
45
|
+
| {
|
|
46
|
+
ok: false;
|
|
47
|
+
status: number;
|
|
48
|
+
error: string;
|
|
49
|
+
};
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function canAccessInsightsCms(role: string | null | undefined): boolean {
|
|
45
|
-
const normalizedRole = normalizeGlobalRole(role);
|
|
46
|
-
return normalizedRole !== null && INSIGHTS_CMS_ALLOWED_ROLES.some((allowedRole) => allowedRole === normalizedRole);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function resolvePostLoginPath(role: string | null | undefined): "/dashboard" | "/account" {
|
|
50
|
-
const normalizedRole = normalizeGlobalRole(role);
|
|
51
|
-
const shouldLandOnDashboard = normalizedRole !== null
|
|
52
|
-
&& DASHBOARD_LANDING_ROLES.some((allowedRole) => allowedRole === normalizedRole);
|
|
53
|
-
return shouldLandOnDashboard ? "/dashboard" : "/account";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function requireServerRoleAccess(allowedRoles: GlobalRole | GlobalRole[]): Promise<ServerRoleAccess> {
|
|
51
|
+
export async function requireServerUserAccess(): Promise<ServerUserAccess> {
|
|
57
52
|
const supabase = await createServerSupabase();
|
|
58
53
|
const {
|
|
59
54
|
data: { user },
|
|
@@ -64,9 +59,81 @@ export async function requireServerRoleAccess(allowedRoles: GlobalRole | GlobalR
|
|
|
64
59
|
return { ok: false, status: 401, error: "Não autorizado." };
|
|
65
60
|
}
|
|
66
61
|
|
|
62
|
+
const { profileId, error: profileError } = await getProfileIdForUser(supabase, user.id);
|
|
63
|
+
if (!profileId) {
|
|
64
|
+
return { ok: false, status: 409, error: profileError ?? "Perfil em falta." };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
67
|
const { data: roleRaw } = await supabase.rpc("current_global_role");
|
|
68
68
|
const role = normalizeGlobalRole(typeof roleRaw === "string" ? roleRaw : null);
|
|
69
69
|
|
|
70
|
+
return { ok: true, supabase, user, profileId, role };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function requireServerPageAccess(): Promise<{
|
|
74
|
+
supabase: Awaited<ReturnType<typeof createServerSupabase>>;
|
|
75
|
+
user: User;
|
|
76
|
+
profileId: string;
|
|
77
|
+
role: GlobalRole | null;
|
|
78
|
+
}> {
|
|
79
|
+
const access = await requireServerUserAccess();
|
|
80
|
+
|
|
81
|
+
if (!access.ok) {
|
|
82
|
+
if (access.status === 401) {
|
|
83
|
+
redirect("/login");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (access.status === 409) {
|
|
87
|
+
redirect("/account");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw new Error(access.error);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return access;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function requireServerPageRoleAccess(
|
|
97
|
+
allowedRoles: GlobalRole | GlobalRole[],
|
|
98
|
+
): Promise<{
|
|
99
|
+
supabase: Awaited<ReturnType<typeof createServerSupabase>>;
|
|
100
|
+
user: User;
|
|
101
|
+
role: GlobalRole;
|
|
102
|
+
}> {
|
|
103
|
+
const access = await requireServerUserAccess();
|
|
104
|
+
|
|
105
|
+
if (!access.ok) {
|
|
106
|
+
if (access.status === 401) {
|
|
107
|
+
redirect("/login");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (access.status === 409) {
|
|
111
|
+
redirect("/account");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
throw new Error(access.error);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
|
|
118
|
+
if (!access.role || !allowed.includes(access.role)) {
|
|
119
|
+
redirect(resolvePostLoginPath(access.role));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
supabase: access.supabase,
|
|
124
|
+
user: access.user,
|
|
125
|
+
role: access.role,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function requireServerRoleAccess(allowedRoles: GlobalRole | GlobalRole[]): Promise<ServerRoleAccess> {
|
|
130
|
+
const access = await requireServerUserAccess();
|
|
131
|
+
if (!access.ok) {
|
|
132
|
+
return access;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { supabase, user, role } = access;
|
|
136
|
+
|
|
70
137
|
if (!role) {
|
|
71
138
|
return { ok: false, status: 403, error: "Acesso proibido." };
|
|
72
139
|
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export const AUTH_RESEND_COOLDOWN_SECONDS = 60;
|
|
2
|
+
|
|
3
|
+
export interface PasswordValidationResult {
|
|
4
|
+
isValid: boolean;
|
|
5
|
+
errors: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type GlobalRole = "client" | "staff" | "admin";
|
|
9
|
+
|
|
10
|
+
const INSIGHTS_CMS_ALLOWED_ROLES: GlobalRole[] = ["admin"];
|
|
11
|
+
const DASHBOARD_LANDING_ROLES: GlobalRole[] = ["staff", "admin"];
|
|
12
|
+
|
|
13
|
+
export function getAuthBaseUrl(): string {
|
|
14
|
+
const configuredUrl = process.env.NEXT_PUBLIC_APP_URL?.trim();
|
|
15
|
+
if (!configuredUrl) {
|
|
16
|
+
throw new Error("Falta NEXT_PUBLIC_APP_URL para redirecionamentos de email de autenticação.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return new URL(configuredUrl).toString().replace(/\/$/, "");
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error("NEXT_PUBLIC_APP_URL inválido. Esperado URL absoluto (ex.: https://exemplo.com).");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildSignupCallbackUrl(): string {
|
|
27
|
+
return new URL("auth/callback", `${getAuthBaseUrl()}/`).toString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildResetPasswordRedirectUrl(): string {
|
|
31
|
+
return `${getAuthBaseUrl()}/reset-password`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function validatePassword(password: string): PasswordValidationResult {
|
|
35
|
+
const errors: string[] = [];
|
|
36
|
+
|
|
37
|
+
if (password.length < 8) {
|
|
38
|
+
errors.push("A palavra-passe deve ter pelo menos 8 caracteres");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!/[A-Z]/.test(password)) {
|
|
42
|
+
errors.push("A palavra-passe deve conter pelo menos uma letra maiúscula");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!/[a-z]/.test(password)) {
|
|
46
|
+
errors.push("A palavra-passe deve conter pelo menos uma letra minúscula");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!/[0-9]/.test(password)) {
|
|
50
|
+
errors.push("A palavra-passe deve conter pelo menos um número");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
isValid: errors.length === 0,
|
|
55
|
+
errors,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateEmail(email: string): boolean {
|
|
60
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
61
|
+
return emailRegex.test(email.trim().toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizeGlobalRole(value: string | null | undefined): GlobalRole | null {
|
|
65
|
+
const normalizedValue = (value ?? "").trim().toLowerCase();
|
|
66
|
+
if (normalizedValue === "client" || normalizedValue === "staff" || normalizedValue === "admin") {
|
|
67
|
+
return normalizedValue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function canAccessInsightsCms(role: string | null | undefined): boolean {
|
|
74
|
+
const normalizedRole = normalizeGlobalRole(role);
|
|
75
|
+
return normalizedRole !== null && INSIGHTS_CMS_ALLOWED_ROLES.some((allowedRole) => allowedRole === normalizedRole);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolvePostLoginPath(role: string | null | undefined): "/dashboard" | "/account" {
|
|
79
|
+
const normalizedRole = normalizeGlobalRole(role);
|
|
80
|
+
const shouldLandOnDashboard = normalizedRole !== null
|
|
81
|
+
&& DASHBOARD_LANDING_ROLES.some((allowedRole) => allowedRole === normalizedRole);
|
|
82
|
+
return shouldLandOnDashboard ? "/dashboard" : "/account";
|
|
83
|
+
}
|
package/src/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./client";
|