@brightweblabs/core-auth 0.1.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 +30 -0
- package/src/client.ts +109 -0
- package/src/index.ts +1 -0
- package/src/server.ts +124 -0
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@brightweblabs/core-auth",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src"
|
|
9
|
+
],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/BWLeoRibeiro/brightweb-platform.git"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts",
|
|
20
|
+
"./client": "./src/client.ts",
|
|
21
|
+
"./server": "./src/server.ts"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@brightweblabs/infra": "workspace:*",
|
|
25
|
+
"@supabase/supabase-js": "^2.89.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "^19.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export const AUTH_RESEND_COOLDOWN_SECONDS = 60;
|
|
6
|
+
|
|
7
|
+
export interface PasswordValidationResult {
|
|
8
|
+
isValid: boolean;
|
|
9
|
+
errors: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CooldownTimerResult {
|
|
13
|
+
remaining: number;
|
|
14
|
+
isCoolingDown: boolean;
|
|
15
|
+
start: () => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
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
|
+
export function useCooldownTimer(durationSeconds: number): CooldownTimerResult {
|
|
71
|
+
const [remaining, setRemaining] = useState(0);
|
|
72
|
+
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
73
|
+
|
|
74
|
+
const clearTimer = useCallback(() => {
|
|
75
|
+
if (intervalRef.current) {
|
|
76
|
+
clearInterval(intervalRef.current);
|
|
77
|
+
intervalRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
const reset = useCallback(() => {
|
|
82
|
+
clearTimer();
|
|
83
|
+
setRemaining(0);
|
|
84
|
+
}, [clearTimer]);
|
|
85
|
+
|
|
86
|
+
const start = useCallback(() => {
|
|
87
|
+
setRemaining(durationSeconds);
|
|
88
|
+
clearTimer();
|
|
89
|
+
|
|
90
|
+
intervalRef.current = setInterval(() => {
|
|
91
|
+
setRemaining((current) => {
|
|
92
|
+
if (current <= 1) {
|
|
93
|
+
clearTimer();
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
return current - 1;
|
|
97
|
+
});
|
|
98
|
+
}, 1000);
|
|
99
|
+
}, [clearTimer, durationSeconds]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => reset, [reset]);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
remaining,
|
|
105
|
+
isCoolingDown: remaining > 0,
|
|
106
|
+
start,
|
|
107
|
+
reset,
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./client";
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { User } from "@supabase/supabase-js";
|
|
2
|
+
import { createServerSupabase } from "@brightweblabs/infra/server";
|
|
3
|
+
|
|
4
|
+
export type GlobalRole = "client" | "staff" | "admin";
|
|
5
|
+
|
|
6
|
+
type Profile = {
|
|
7
|
+
id: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ServerAccess = {
|
|
11
|
+
user: User | null;
|
|
12
|
+
profile: Profile | null;
|
|
13
|
+
role: GlobalRole | null;
|
|
14
|
+
isStaff: boolean;
|
|
15
|
+
isAdmin: boolean;
|
|
16
|
+
canAccessInsightsCms: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ServerRoleAccess =
|
|
20
|
+
| {
|
|
21
|
+
ok: true;
|
|
22
|
+
supabase: Awaited<ReturnType<typeof createServerSupabase>>;
|
|
23
|
+
user: User;
|
|
24
|
+
role: GlobalRole;
|
|
25
|
+
}
|
|
26
|
+
| {
|
|
27
|
+
ok: false;
|
|
28
|
+
status: number;
|
|
29
|
+
error: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const INSIGHTS_CMS_ALLOWED_ROLES: GlobalRole[] = ["admin"];
|
|
33
|
+
const DASHBOARD_LANDING_ROLES: GlobalRole[] = ["staff", "admin"];
|
|
34
|
+
|
|
35
|
+
export function normalizeGlobalRole(value: string | null | undefined): GlobalRole | null {
|
|
36
|
+
const normalizedValue = (value ?? "").trim().toLowerCase();
|
|
37
|
+
if (normalizedValue === "client" || normalizedValue === "staff" || normalizedValue === "admin") {
|
|
38
|
+
return normalizedValue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
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> {
|
|
57
|
+
const supabase = await createServerSupabase();
|
|
58
|
+
const {
|
|
59
|
+
data: { user },
|
|
60
|
+
error: authError,
|
|
61
|
+
} = await supabase.auth.getUser();
|
|
62
|
+
|
|
63
|
+
if (authError || !user) {
|
|
64
|
+
return { ok: false, status: 401, error: "Não autorizado." };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { data: roleRaw } = await supabase.rpc("current_global_role");
|
|
68
|
+
const role = normalizeGlobalRole(typeof roleRaw === "string" ? roleRaw : null);
|
|
69
|
+
|
|
70
|
+
if (!role) {
|
|
71
|
+
return { ok: false, status: 403, error: "Acesso proibido." };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allowed = Array.isArray(allowedRoles) ? allowedRoles : [allowedRoles];
|
|
75
|
+
if (!allowed.includes(role)) {
|
|
76
|
+
return { ok: false, status: 403, error: "Acesso proibido." };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { ok: true, supabase, user, role };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function getProfileIdForUser(
|
|
83
|
+
supabase: Awaited<ReturnType<typeof createServerSupabase>>,
|
|
84
|
+
userId: string,
|
|
85
|
+
): Promise<{ profileId: string | null; error: string | null }> {
|
|
86
|
+
const { data: profile, error } = await supabase
|
|
87
|
+
.from("profiles")
|
|
88
|
+
.select("id")
|
|
89
|
+
.eq("user_id", userId)
|
|
90
|
+
.maybeSingle<{ id: string }>();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
profileId: profile?.id ?? null,
|
|
94
|
+
error: error?.message ?? null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getServerAccess(): Promise<ServerAccess> {
|
|
99
|
+
const supabase = await createServerSupabase();
|
|
100
|
+
const {
|
|
101
|
+
data: { user },
|
|
102
|
+
error: authError,
|
|
103
|
+
} = await supabase.auth.getUser();
|
|
104
|
+
|
|
105
|
+
if (authError || !user) {
|
|
106
|
+
return { user: null, profile: null, role: null, isStaff: false, isAdmin: false, canAccessInsightsCms: false };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { profileId } = await getProfileIdForUser(supabase, user.id);
|
|
110
|
+
|
|
111
|
+
const { data: roleValue } = await supabase.rpc("current_global_role");
|
|
112
|
+
const role = normalizeGlobalRole(typeof roleValue === "string" ? roleValue : null);
|
|
113
|
+
const isStaff = resolvePostLoginPath(role) === "/dashboard";
|
|
114
|
+
const isAdmin = role === "admin";
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
user,
|
|
118
|
+
profile: profileId ? { id: profileId } : null,
|
|
119
|
+
role,
|
|
120
|
+
isStaff,
|
|
121
|
+
isAdmin,
|
|
122
|
+
canAccessInsightsCms: canAccessInsightsCms(role),
|
|
123
|
+
};
|
|
124
|
+
}
|