@geekapps/auth-nextjs 0.3.10 → 0.5.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 +49 -2
- package/dist/actions.js +17 -7
- package/dist/actions.js.map +1 -1
- package/dist/client.d.ts +196 -2
- package/dist/client.js +174 -3
- package/dist/client.js.map +1 -1
- package/dist/middleware.js +22 -12
- package/dist/middleware.js.map +1 -1
- package/dist/server.d.ts +13 -4
- package/dist/server.js +9 -3
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,16 +46,63 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|
|
46
46
|
Hooks principais:
|
|
47
47
|
|
|
48
48
|
- `useAuth()`
|
|
49
|
-
- `useOrg()`
|
|
49
|
+
- `useOrg()` — organizações do próprio usuário (as que ele já é membro/dono)
|
|
50
|
+
- `useAccounts()`
|
|
50
51
|
- `useUsers()`
|
|
51
52
|
- `useRoles()`
|
|
52
53
|
- `usePermissions()`
|
|
53
54
|
- `useSessions()`
|
|
55
|
+
- `useMembers()` — membros e convites de uma organização
|
|
56
|
+
- `usePublicOrganizations()` — descoberta de organizações públicas do app, separado de `useOrg()`
|
|
57
|
+
- `useJoinRequests()` — solicitações de entrada em organizações públicas
|
|
58
|
+
- `useInvites()` — aceite/recusa de convite por email neste próprio app
|
|
54
59
|
|
|
60
|
+
### Organizações: membros, convites e organizações públicas
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { useMembers } from "@geekapps/auth-nextjs/client";
|
|
64
|
+
|
|
65
|
+
const { listMembers, addMember, updateMember, inviteMember, listInvites, revokeInvite } = useMembers();
|
|
66
|
+
|
|
67
|
+
const members = await listMembers(org.id);
|
|
68
|
+
|
|
69
|
+
// redirectUri é obrigatório — precisa estar cadastrado em Application.redirectUris,
|
|
70
|
+
// e este próprio app implementa a rota que trata o ?invite_token= recebido lá.
|
|
71
|
+
const result = await inviteMember(org.id, {
|
|
72
|
+
email: "novo@empresa.com",
|
|
73
|
+
roleId: "role-uuid",
|
|
74
|
+
redirectUri: "https://meuapp.com/invites/accept",
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { useInvites } from "@geekapps/auth-nextjs/client";
|
|
80
|
+
|
|
81
|
+
// Na sua própria rota /invites/accept?invite_token=...
|
|
82
|
+
const { getInvitePreview, acceptInvite, rejectInvite } = useInvites();
|
|
83
|
+
|
|
84
|
+
const preview = await getInvitePreview(token); // funciona sem sessão
|
|
85
|
+
if (!preview.alreadyAccepted && !preview.currentAccount) {
|
|
86
|
+
// sem sessão — chame signIn() (de useAuth()) e volte pra essa rota depois
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = await acceptInvite(token); // loginRequired:true se não houver sessão
|
|
90
|
+
await rejectInvite(token); // não exige login
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { usePublicOrganizations, useJoinRequests } from "@geekapps/auth-nextjs/client";
|
|
95
|
+
|
|
96
|
+
const { listPublicOrganizations } = usePublicOrganizations();
|
|
97
|
+
const publicOrgs = await listPublicOrganizations();
|
|
98
|
+
|
|
99
|
+
const { requestToJoin, getMyJoinRequest, listJoinRequests, approveJoinRequest } = useJoinRequests();
|
|
100
|
+
await requestToJoin(org.id, { message: "Trabalho na região" });
|
|
101
|
+
```
|
|
55
102
|
|
|
56
103
|
## Standalone client auth actions
|
|
57
104
|
|
|
58
|
-
`@geekapps/auth-nextjs/client` exporta funções standalone usadas pela `auth-
|
|
105
|
+
`@geekapps/auth-nextjs/client` exporta funções standalone usadas pela `auth-screens`:
|
|
59
106
|
|
|
60
107
|
```ts
|
|
61
108
|
import {
|
package/dist/actions.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
|
|
3
3
|
// src/actions.ts
|
|
4
|
+
import { cookies as cookies2, headers as headers2 } from "next/headers";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
4
7
|
import { cookies, headers } from "next/headers";
|
|
8
|
+
import { redirect } from "next/navigation";
|
|
9
|
+
import { NextResponse } from "next/server";
|
|
10
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
11
|
+
var GEEKAPPS_AUTH_SERVER_URL = "https://accounts-api.geekapps.com.br";
|
|
12
|
+
|
|
13
|
+
// src/actions.ts
|
|
5
14
|
function env(name) {
|
|
6
15
|
const value = process.env[name];
|
|
7
16
|
return value && value.trim().length > 0 ? value : void 0;
|
|
@@ -11,7 +20,8 @@ function normalizeBaseUrl(url) {
|
|
|
11
20
|
}
|
|
12
21
|
function resolveIssuer() {
|
|
13
22
|
return normalizeBaseUrl(
|
|
14
|
-
|
|
23
|
+
// Deprecated: kept for backwards compatibility during the auth-server rename.
|
|
24
|
+
env("GEEKAPPS_AUTH_ISSUER") ?? env("GEEKAPPS_AUTH_API_URL") ?? GEEKAPPS_AUTH_SERVER_URL
|
|
15
25
|
);
|
|
16
26
|
}
|
|
17
27
|
function appendClientCredentials(url) {
|
|
@@ -57,12 +67,12 @@ async function getCookieNames() {
|
|
|
57
67
|
}
|
|
58
68
|
async function getActiveAccessToken() {
|
|
59
69
|
try {
|
|
60
|
-
const hdrs = await
|
|
70
|
+
const hdrs = await headers2();
|
|
61
71
|
const auth = hdrs.get("authorization");
|
|
62
72
|
if (auth?.startsWith("Bearer ")) return auth.slice(7);
|
|
63
73
|
} catch {
|
|
64
74
|
}
|
|
65
|
-
const cookieStore = await
|
|
75
|
+
const cookieStore = await cookies2();
|
|
66
76
|
const cn = await getCookieNames();
|
|
67
77
|
const raw = cookieStore.get(cn.active)?.value;
|
|
68
78
|
const activeIdx = raw ? Number.parseInt(raw, 10) : 0;
|
|
@@ -73,7 +83,7 @@ async function getActiveAccessToken() {
|
|
|
73
83
|
return cookieStore.get(cn.slotAt(idx))?.value ?? null;
|
|
74
84
|
}
|
|
75
85
|
async function getActiveRefreshToken() {
|
|
76
|
-
const cookieStore = await
|
|
86
|
+
const cookieStore = await cookies2();
|
|
77
87
|
const cn = await getCookieNames();
|
|
78
88
|
const raw = cookieStore.get(cn.active)?.value;
|
|
79
89
|
const activeIdx = raw ? Number.parseInt(raw, 10) : 0;
|
|
@@ -104,7 +114,7 @@ async function refreshAndSaveTokens() {
|
|
|
104
114
|
const newAccessToken = data.accessToken ?? data.access_token;
|
|
105
115
|
const newRefreshToken = data.refreshToken ?? data.refresh_token;
|
|
106
116
|
if (!newAccessToken) return null;
|
|
107
|
-
const cookieStore = await
|
|
117
|
+
const cookieStore = await cookies2();
|
|
108
118
|
const cn = await getCookieNames();
|
|
109
119
|
const raw = cookieStore.get(cn.active)?.value;
|
|
110
120
|
const activeIdx = raw ? Number.parseInt(raw, 10) : 1;
|
|
@@ -158,7 +168,7 @@ async function getMe() {
|
|
|
158
168
|
}
|
|
159
169
|
}
|
|
160
170
|
async function getAccounts() {
|
|
161
|
-
const cookieStore = await
|
|
171
|
+
const cookieStore = await cookies2();
|
|
162
172
|
const cn = await getCookieNames();
|
|
163
173
|
const countRaw = cookieStore.get(cn.count)?.value;
|
|
164
174
|
const count = countRaw ? Number.parseInt(countRaw, 10) : 0;
|
|
@@ -187,7 +197,7 @@ async function getAccounts() {
|
|
|
187
197
|
}
|
|
188
198
|
async function getOrganizations() {
|
|
189
199
|
try {
|
|
190
|
-
const cookieStore = await
|
|
200
|
+
const cookieStore = await cookies2();
|
|
191
201
|
const cn = await getCookieNames();
|
|
192
202
|
const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;
|
|
193
203
|
const data = await authFetch("/organizations");
|
package/dist/actions.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/actions.ts"],"sourcesContent":["\"use server\";\n\nimport { cookies, headers } from \"next/headers\";\nimport type { ConnectedAccount, Organization, User } from \"./client\";\n\n// ── cookie helpers (duplicated from server.ts to keep this file self-contained) ──\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction resolveIssuer() {\n return normalizeBaseUrl(\n env(\"GEEKAPPS_AUTH_ISSUER\") ?? env(\"GEEKAPPS_AUTH_API_URL\") ?? \"http://localhost:3020\",\n );\n}\n\nfunction appendClientCredentials(url: string): string {\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\");\n const clientSecret = env(\"GEEKAPPS_AUTH_CLIENT_SECRET\");\n if (!clientId || !clientSecret) return url;\n const u = new URL(url);\n u.searchParams.set(\"client_id\", clientId);\n u.searchParams.set(\"client_secret\", clientSecret);\n return u.toString();\n}\n\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\nfunction deriveCookiePrefix(): string {\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\ntype SlotMeta = { id: string; sid?: string; n?: string; e?: string; p?: string };\n\nfunction decodeMeta(value: string): SlotMeta | null {\n try {\n const m = JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")) as SlotMeta;\n return m.id ? m : null;\n } catch {\n return null;\n }\n}\n\nasync function getCookieNames() {\n const prefix = deriveCookiePrefix();\n return {\n active: `${prefix}_active`,\n count: `${prefix}_count`,\n activeOrganizationId: `${prefix}_ao`,\n slotMeta: (n: number) => `${prefix}_m${n}`,\n slotAt: (n: number) => `${prefix}_at${n}`,\n slotRt: (n: number) => `${prefix}_rt${n}`,\n };\n}\n\nasync function getActiveAccessToken(): Promise<string | null> {\n // Prefer the token injected by the middleware via request header (same-request refresh)\n try {\n const hdrs = await headers();\n const auth = hdrs.get(\"authorization\");\n if (auth?.startsWith(\"Bearer \")) return auth.slice(7);\n } catch {}\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const raw = cookieStore.get(cn.active)?.value;\n const activeIdx = raw ? Number.parseInt(raw, 10) : 0;\n const countRaw = cookieStore.get(cn.count)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n if (count === 0) return null;\n const idx = activeIdx >= 1 && activeIdx <= count ? activeIdx : 1;\n return cookieStore.get(cn.slotAt(idx))?.value ?? null;\n}\n\nasync function getActiveRefreshToken(): Promise<string | null> {\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const raw = cookieStore.get(cn.active)?.value;\n const activeIdx = raw ? Number.parseInt(raw, 10) : 0;\n const countRaw = cookieStore.get(cn.count)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n if (count === 0) return null;\n const idx = activeIdx >= 1 && activeIdx <= count ? activeIdx : 1;\n return cookieStore.get(cn.slotRt(idx))?.value ?? null;\n}\n\nasync function refreshAndSaveTokens(): Promise<string | null> {\n const refreshToken = await getActiveRefreshToken();\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n const clientSecret = env(\"GEEKAPPS_AUTH_CLIENT_SECRET\") ?? \"\";\n if (!refreshToken || !clientId) return null;\n\n const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\");\n const response = await fetch(`${resolveIssuer()}/auth/token`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({ grant_type: \"refresh_token\", refresh_token: refreshToken }),\n cache: \"no-store\",\n });\n\n if (!response.ok) return null;\n\n const data = await response.json().catch(() => null) as Record<string, unknown> | null;\n if (!data) return null;\n\n const newAccessToken =\n (data.accessToken ?? data.access_token) as string | undefined;\n const newRefreshToken =\n (data.refreshToken ?? data.refresh_token) as string | undefined;\n\n if (!newAccessToken) return null;\n\n // Update the active slot cookies\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const raw = cookieStore.get(cn.active)?.value;\n const activeIdx = raw ? Number.parseInt(raw, 10) : 1;\n const opts = {\n httpOnly: true,\n secure: process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n maxAge: 60 * 60 * 24 * 30,\n };\n cookieStore.set(cn.slotAt(activeIdx), newAccessToken, opts);\n if (newRefreshToken) cookieStore.set(cn.slotRt(activeIdx), newRefreshToken, opts);\n // Update public token cookie\n cookieStore.set(`${deriveCookiePrefix()}_tp`, newAccessToken, {\n httpOnly: false,\n secure: process.env.NODE_ENV === \"production\",\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 60 * 60 * 24 * 30,\n });\n\n return newAccessToken;\n}\n\nasync function authFetch<T>(path: string): Promise<T> {\n let accessToken = await getActiveAccessToken();\n if (!accessToken) throw new Error(\"Not authenticated\");\n\n const issuer = resolveIssuer();\n const doFetch = (token: string) =>\n fetch(appendClientCredentials(`${issuer}${path}`), {\n headers: { authorization: `Bearer ${token}` },\n cache: \"no-store\",\n });\n\n let response = await doFetch(accessToken);\n\n if (response.status === 401) {\n const newToken = await refreshAndSaveTokens();\n if (!newToken) throw new Error(\"Session expired\");\n response = await doFetch(newToken);\n }\n\n if (!response.ok) {\n const err = await response.json().catch(() => null) as Record<string, unknown> | null;\n throw new Error((err?.error_description ?? err?.error ?? err?.message ?? `Auth error ${response.status}`) as string);\n }\n\n if (response.status === 204) return null as T;\n return response.json() as Promise<T>;\n}\n\n// ── Exported server actions ────────────────────────────────────────────────────\n\nexport async function getMe(): Promise<{ user: User | null }> {\n try {\n const data = await authFetch<{ user: User | null } | User | null>(\"/auth/me\");\n if (!data) return { user: null };\n if (typeof data === \"object\" && \"user\" in data) return data as { user: User | null };\n return { user: data as User | null };\n } catch {\n return { user: null };\n }\n}\n\nexport async function getAccounts(): Promise<{ accounts: ConnectedAccount[] }> {\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const countRaw = cookieStore.get(cn.count)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n const activeRaw = cookieStore.get(cn.active)?.value;\n const activeIdx = activeRaw ? Number.parseInt(activeRaw, 10) : 0;\n\n const accounts: ConnectedAccount[] = [];\n for (let i = 1; i <= count; i++) {\n const at = cookieStore.get(cn.slotAt(i))?.value;\n if (!at) continue;\n const meta = decodeMeta(cookieStore.get(cn.slotMeta(i))?.value ?? \"\");\n if (!meta) continue;\n accounts.push({\n id: meta.id,\n sessionId: meta.sid ?? meta.id,\n isActive: i === activeIdx,\n user: {\n id: meta.id,\n name: meta.n ?? null,\n email: meta.e ?? \"\",\n imageUrl: meta.p ?? null,\n phone: null,\n },\n });\n }\n\n return { accounts };\n}\n\nexport async function getOrganizations(): Promise<{ organizations: Organization[]; currentOrgId?: string | null }> {\n try {\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n const data = await authFetch<Organization[] | { organizations: Organization[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: Organization[] }).organizations;\n const resolvedOrgId = activeOrgId ?? organizations[0]?.id ?? null;\n return { organizations, currentOrgId: resolvedOrgId };\n } catch (err) {\n console.error(\"[getOrganizations] error:\", err);\n return { organizations: [] };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/actions.ts","../src/server.ts"],"sourcesContent":["\"use server\";\n\nimport { cookies, headers } from \"next/headers\";\nimport type { ConnectedAccount, Organization, User } from \"./client\";\nimport { GEEKAPPS_AUTH_SERVER_URL } from \"./server\";\n\n// ── cookie helpers (duplicated from server.ts to keep this file self-contained) ──\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction resolveIssuer() {\n return normalizeBaseUrl(\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ?? env(\"GEEKAPPS_AUTH_API_URL\") ?? GEEKAPPS_AUTH_SERVER_URL,\n );\n}\n\nfunction appendClientCredentials(url: string): string {\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\");\n const clientSecret = env(\"GEEKAPPS_AUTH_CLIENT_SECRET\");\n if (!clientId || !clientSecret) return url;\n const u = new URL(url);\n u.searchParams.set(\"client_id\", clientId);\n u.searchParams.set(\"client_secret\", clientSecret);\n return u.toString();\n}\n\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\nfunction deriveCookiePrefix(): string {\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\ntype SlotMeta = { id: string; sid?: string; n?: string; e?: string; p?: string };\n\nfunction decodeMeta(value: string): SlotMeta | null {\n try {\n const m = JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")) as SlotMeta;\n return m.id ? m : null;\n } catch {\n return null;\n }\n}\n\nasync function getCookieNames() {\n const prefix = deriveCookiePrefix();\n return {\n active: `${prefix}_active`,\n count: `${prefix}_count`,\n activeOrganizationId: `${prefix}_ao`,\n slotMeta: (n: number) => `${prefix}_m${n}`,\n slotAt: (n: number) => `${prefix}_at${n}`,\n slotRt: (n: number) => `${prefix}_rt${n}`,\n };\n}\n\nasync function getActiveAccessToken(): Promise<string | null> {\n // Prefer the token injected by the middleware via request header (same-request refresh)\n try {\n const hdrs = await headers();\n const auth = hdrs.get(\"authorization\");\n if (auth?.startsWith(\"Bearer \")) return auth.slice(7);\n } catch {}\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const raw = cookieStore.get(cn.active)?.value;\n const activeIdx = raw ? Number.parseInt(raw, 10) : 0;\n const countRaw = cookieStore.get(cn.count)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n if (count === 0) return null;\n const idx = activeIdx >= 1 && activeIdx <= count ? activeIdx : 1;\n return cookieStore.get(cn.slotAt(idx))?.value ?? null;\n}\n\nasync function getActiveRefreshToken(): Promise<string | null> {\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const raw = cookieStore.get(cn.active)?.value;\n const activeIdx = raw ? Number.parseInt(raw, 10) : 0;\n const countRaw = cookieStore.get(cn.count)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n if (count === 0) return null;\n const idx = activeIdx >= 1 && activeIdx <= count ? activeIdx : 1;\n return cookieStore.get(cn.slotRt(idx))?.value ?? null;\n}\n\nasync function refreshAndSaveTokens(): Promise<string | null> {\n const refreshToken = await getActiveRefreshToken();\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n const clientSecret = env(\"GEEKAPPS_AUTH_CLIENT_SECRET\") ?? \"\";\n if (!refreshToken || !clientId) return null;\n\n const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\");\n const response = await fetch(`${resolveIssuer()}/auth/token`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({ grant_type: \"refresh_token\", refresh_token: refreshToken }),\n cache: \"no-store\",\n });\n\n if (!response.ok) return null;\n\n const data = await response.json().catch(() => null) as Record<string, unknown> | null;\n if (!data) return null;\n\n const newAccessToken =\n (data.accessToken ?? data.access_token) as string | undefined;\n const newRefreshToken =\n (data.refreshToken ?? data.refresh_token) as string | undefined;\n\n if (!newAccessToken) return null;\n\n // Update the active slot cookies\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const raw = cookieStore.get(cn.active)?.value;\n const activeIdx = raw ? Number.parseInt(raw, 10) : 1;\n const opts = {\n httpOnly: true,\n secure: process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n maxAge: 60 * 60 * 24 * 30,\n };\n cookieStore.set(cn.slotAt(activeIdx), newAccessToken, opts);\n if (newRefreshToken) cookieStore.set(cn.slotRt(activeIdx), newRefreshToken, opts);\n // Update public token cookie\n cookieStore.set(`${deriveCookiePrefix()}_tp`, newAccessToken, {\n httpOnly: false,\n secure: process.env.NODE_ENV === \"production\",\n sameSite: \"lax\",\n path: \"/\",\n maxAge: 60 * 60 * 24 * 30,\n });\n\n return newAccessToken;\n}\n\nasync function authFetch<T>(path: string): Promise<T> {\n let accessToken = await getActiveAccessToken();\n if (!accessToken) throw new Error(\"Not authenticated\");\n\n const issuer = resolveIssuer();\n const doFetch = (token: string) =>\n fetch(appendClientCredentials(`${issuer}${path}`), {\n headers: { authorization: `Bearer ${token}` },\n cache: \"no-store\",\n });\n\n let response = await doFetch(accessToken);\n\n if (response.status === 401) {\n const newToken = await refreshAndSaveTokens();\n if (!newToken) throw new Error(\"Session expired\");\n response = await doFetch(newToken);\n }\n\n if (!response.ok) {\n const err = await response.json().catch(() => null) as Record<string, unknown> | null;\n throw new Error((err?.error_description ?? err?.error ?? err?.message ?? `Auth error ${response.status}`) as string);\n }\n\n if (response.status === 204) return null as T;\n return response.json() as Promise<T>;\n}\n\n// ── Exported server actions ────────────────────────────────────────────────────\n\nexport async function getMe(): Promise<{ user: User | null }> {\n try {\n const data = await authFetch<{ user: User | null } | User | null>(\"/auth/me\");\n if (!data) return { user: null };\n if (typeof data === \"object\" && \"user\" in data) return data as { user: User | null };\n return { user: data as User | null };\n } catch {\n return { user: null };\n }\n}\n\nexport async function getAccounts(): Promise<{ accounts: ConnectedAccount[] }> {\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const countRaw = cookieStore.get(cn.count)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n const activeRaw = cookieStore.get(cn.active)?.value;\n const activeIdx = activeRaw ? Number.parseInt(activeRaw, 10) : 0;\n\n const accounts: ConnectedAccount[] = [];\n for (let i = 1; i <= count; i++) {\n const at = cookieStore.get(cn.slotAt(i))?.value;\n if (!at) continue;\n const meta = decodeMeta(cookieStore.get(cn.slotMeta(i))?.value ?? \"\");\n if (!meta) continue;\n accounts.push({\n id: meta.id,\n sessionId: meta.sid ?? meta.id,\n isActive: i === activeIdx,\n user: {\n id: meta.id,\n name: meta.n ?? null,\n email: meta.e ?? \"\",\n imageUrl: meta.p ?? null,\n phone: null,\n },\n });\n }\n\n return { accounts };\n}\n\nexport async function getOrganizations(): Promise<{ organizations: Organization[]; currentOrgId?: string | null }> {\n try {\n const cookieStore = await cookies();\n const cn = await getCookieNames();\n const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n const data = await authFetch<Organization[] | { organizations: Organization[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: Organization[] }).organizations;\n const resolvedOrgId = activeOrgId ?? organizations[0]?.id ?? null;\n return { organizations, currentOrgId: resolvedOrgId };\n } catch (err) {\n console.error(\"[getOrganizations] error:\", err);\n return { organizations: [] };\n }\n}\n","import { cookies, headers } from \"next/headers\";\nimport { redirect } from \"next/navigation\";\nimport { NextResponse, type NextRequest } from \"next/server\";\nimport { createRemoteJWKSet, jwtVerify, type JWTPayload } from \"jose\";\n\n/**\n * Fixed URL of the Geekapps Auth Server (OIDC issuer + API). This never\n * changes per-environment, so it is hardcoded rather than read from env.\n */\nexport const GEEKAPPS_AUTH_SERVER_URL = \"https://accounts-api.geekapps.com.br\";\n\nexport type GeekappsAuthConfig = {\n /**\n * Auth Server (OIDC) issuer URL. Defaults to GEEKAPPS_AUTH_SERVER_URL and\n * should not need overriding in normal use.\n *\n * Deprecated (still supported as a fallback): GEEKAPPS_AUTH_SERVER_ISSUER,\n * GEEKAPPS_AUTH_ISSUER, GEEKAPPS_AUTH_SERVER_URL env, GEEKAPPS_AUTH_API_URL\n */\n issuer?: string;\n\n /**\n * OAuth client id. Optional when GEEKAPPS_AUTH_CLIENT_ID is defined.\n */\n clientId?: string;\n\n /**\n * OAuth client secret for confidential clients. Optional when\n * GEEKAPPS_AUTH_CLIENT_SECRET is defined.\n */\n clientSecret?: string;\n\n /**\n * Callback URL. Optional when GEEKAPPS_AUTH_REDIRECT_URI is defined.\n * If omitted, it is inferred from the current request origin + basePath/callback.\n */\n redirectUri?: string;\n\n scopes?: string[];\n basePath?: string;\n /**\n * @deprecated cookiePrefix is now auto-derived from clientId.\n * This field is kept for backwards compatibility but is ignored.\n */\n cookiePrefix?: string;\n secureCookies?: boolean;\n\n /**\n * Friendly alias used by apps. Same as afterSignInRedirectTo.\n */\n redirectTo?: string;\n\n afterSignInRedirectTo?: string;\n afterSignOutRedirectTo?: string;\n};\n\ntype ResolvedGeekappsAuthConfig = GeekappsAuthConfig & {\n issuer: string;\n clientId: string;\n basePath: string;\n cookiePrefix: string;\n};\n\nexport type SessionTokens = {\n accessToken: string | null;\n refreshToken: string | null;\n idToken: string | null;\n sessionId: string | null;\n};\n\nexport type StoredAccount = {\n /** Stable identifier — `sub` from id_token or fallback to sessionId. */\n id: string;\n accessToken: string;\n refreshToken: string | null;\n idToken: string | null;\n sessionId: string | null;\n user: {\n sub: string;\n name?: string | null;\n email?: string | null;\n picture?: string | null;\n phone?: string | null;\n };\n};\n\nexport type ServerAuthApiClient = {\n get: <T>(path: string) => Promise<T>;\n post: <T>(path: string, body?: unknown) => Promise<T>;\n patch: <T>(path: string, body?: unknown) => Promise<T>;\n put: <T>(path: string, body?: unknown) => Promise<T>;\n delete: <T>(path: string) => Promise<T>;\n};\n\ntype JsonObject = Record<string, unknown>;\n\ntype TokenLikeResponse = JsonObject & {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n session_id?: string;\n sessionId?: string;\n tokens?: {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n session_id?: string;\n sessionId?: string;\n };\n};\n\nconst MAX_ACCOUNTS = 8;\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction normalizeBasePath(path = \"/api/auth\") {\n if (!path.startsWith(\"/\")) return `/${path}`;\n return path.replace(/\\/$/, \"\");\n}\n\nfunction createUrl(baseUrl: string, path: string) {\n if (/^https?:\\/\\//.test(path)) return path;\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n return `${normalizeBaseUrl(baseUrl)}${normalizedPath}`;\n}\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction resolveIssuer(input: GeekappsAuthConfig) {\n return normalizeBaseUrl(\n input.issuer ??\n env(\"GEEKAPPS_AUTH_SERVER_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_SERVER_URL\") ??\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL,\n );\n}\n\nfunction resolveClientId(input: GeekappsAuthConfig) {\n return input.clientId ?? env(\"GEEKAPPS_AUTH_CLIENT_ID\") ?? \"\";\n}\n\nfunction resolveClientSecret(input: GeekappsAuthConfig) {\n return input.clientSecret ?? env(\"GEEKAPPS_AUTH_CLIENT_SECRET\");\n}\n\nfunction applyClientCredentials(url: URL, config: ResolvedGeekappsAuthConfig) {\n if (!config.clientId || !config.clientSecret) return;\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"client_secret\", config.clientSecret);\n}\n\nfunction resolveConfiguredRedirectUri(input: GeekappsAuthConfig) {\n return input.redirectUri ?? env(\"GEEKAPPS_AUTH_REDIRECT_URI\");\n}\n\n/**\n * Derives a short stable 8-char hex hash from a clientId string.\n * Uses a simple djb2-style hash — no crypto dependency needed.\n */\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0; // keep unsigned 32-bit\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\n/**\n * Auto-derives the cookie prefix from clientId.\n * Format: `gk_{8-char-hash}` — guaranteed unique per OAuth client.\n * Falls back to `gk` when clientId is empty (dev/test environments).\n */\nfunction deriveCookiePrefix(clientId: string): string {\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\nfunction requestOrigin(request: NextRequest) {\n const configured = env(\"GEEKAPPS_APP_URL\");\n if (configured) return configured.replace(/\\/$/, \"\");\n\n throw new Error(\n \"Cannot determine app origin. Set GEEKAPPS_APP_URL.\",\n );\n}\n\nfunction resolveRedirectUri(config: ResolvedGeekappsAuthConfig, request: NextRequest) {\n return config.redirectUri ?? `${requestOrigin(request)}${config.basePath}/callback`;\n}\n\nfunction tokenFromResponse(data: TokenLikeResponse): SessionTokens {\n return {\n accessToken: data.accessToken ?? data.access_token ?? data.tokens?.accessToken ?? data.tokens?.access_token ?? null,\n refreshToken:\n data.refreshToken ?? data.refresh_token ?? data.tokens?.refreshToken ?? data.tokens?.refresh_token ?? null,\n idToken: data.idToken ?? data.id_token ?? data.tokens?.idToken ?? data.tokens?.id_token ?? null,\n sessionId: data.sessionId ?? data.session_id ?? data.tokens?.sessionId ?? data.tokens?.session_id ?? null,\n };\n}\n\n/** Decode a JWT payload without verifying signature (safe for client-visible claims). */\nfunction decodeJwtPayload(token: string): Record<string, unknown> | null {\n try {\n const parts = token.split(\".\");\n if (parts.length < 2) return null;\n const payload = parts[1];\n if (!payload) return null;\n return JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction parseAccountFromJwt(tokens: SessionTokens): StoredAccount | null {\n if (!tokens.accessToken) return null;\n\n const jwtToParse = tokens.idToken ?? tokens.accessToken;\n const payload = decodeJwtPayload(jwtToParse);\n if (!payload) return null;\n\n const sub = typeof payload.sub === \"string\" ? payload.sub : null;\n const id = sub ?? tokens.sessionId;\n if (!id) return null;\n\n return {\n id,\n accessToken: tokens.accessToken,\n refreshToken: tokens.refreshToken,\n idToken: tokens.idToken ?? null,\n sessionId: tokens.sessionId,\n user: {\n sub: id,\n name: typeof payload.name === \"string\" ? payload.name : null,\n email: typeof payload.email === \"string\" ? payload.email : null,\n picture: typeof payload.picture === \"string\" ? payload.picture : null,\n phone: typeof payload.phone_number === \"string\" ? payload.phone_number : null,\n },\n };\n}\n\n/**\n * Cookie name scheme — 3 cookies per slot, each well under 4096 bytes:\n *\n * {prefix}_m{n} — metadata blob (base64url JSON): { id, sid?, n?, e?, p? }\n * Claims extracted from id_token at login. Never stores tokens.\n * {prefix}_at{n} — access token (httpOnly). ~1-2 KB.\n * {prefix}_rt{n} — refresh token (httpOnly). ~1 KB.\n * {prefix}_active — active slot index (\"1\", \"2\", …)\n * {prefix}_count — total stored slots\n * {prefix}_ao — active org id for this app\n * {prefix}_tp — non-httpOnly copy of active access token for client-side axios\n *\n * id_token is NOT stored in cookies — claims are extracted at login and cached in\n * the metadata cookie. This is the key size saving.\n */\nfunction getCookieNames(prefix: string) {\n return {\n active: `${prefix}_active`,\n count: `${prefix}_count`,\n activeOrganizationId: `${prefix}_ao`,\n tokenPub: `${prefix}_tp`,\n slotMeta: (n: number) => `${prefix}_m${n}`,\n slotAt: (n: number) => `${prefix}_at${n}`,\n slotRt: (n: number) => `${prefix}_rt${n}`,\n };\n}\n\ntype SlotMeta = {\n id: string;\n sid?: string;\n n?: string;\n e?: string;\n p?: string;\n};\n\nfunction encodeMeta(account: StoredAccount): string {\n const m: SlotMeta = { id: account.id };\n if (account.sessionId) m.sid = account.sessionId;\n if (account.user.name) m.n = account.user.name;\n if (account.user.email) m.e = account.user.email;\n if (account.user.picture) m.p = account.user.picture;\n return Buffer.from(JSON.stringify(m), \"utf8\").toString(\"base64url\");\n}\n\nfunction decodeMeta(value: string): SlotMeta | null {\n try {\n const m = JSON.parse(Buffer.from(value, \"base64url\").toString(\"utf8\")) as SlotMeta;\n return m.id ? m : null;\n } catch {\n return null;\n }\n}\n\nfunction getCookieOptions(config: ResolvedGeekappsAuthConfig) {\n return {\n httpOnly: true,\n secure: config.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n };\n}\n\nasync function readBody(request: NextRequest) {\n const text = await request.text();\n if (!text) return undefined;\n try {\n return JSON.parse(text);\n } catch {\n return text;\n }\n}\n\nfunction appendSearchParams(url: string, request: NextRequest) {\n const target = new URL(url);\n request.nextUrl.searchParams.forEach((value, key) => {\n target.searchParams.set(key, value);\n });\n return target.toString();\n}\n\nfunction routeFromRequest(request: NextRequest, basePath: string) {\n const pathname = request.nextUrl.pathname;\n if (!pathname.startsWith(basePath)) return pathname;\n const route = pathname.slice(basePath.length);\n return route || \"/\";\n}\n\nfunction isJsonResponse(response: Response) {\n return response.headers.get(\"content-type\")?.includes(\"application/json\");\n}\n\nasync function responseFromApi(response: Response) {\n if (response.status === 204) return new NextResponse(null, { status: 204 });\n const body = isJsonResponse(response) ? await response.json().catch(() => null) : await response.text();\n return NextResponse.json(body, { status: response.status });\n}\n\nconst _remoteJWKSCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\ntype ServiceTokenCache = { token: string; expiresAt: number };\nconst _serviceTokenCache = new Map<string, ServiceTokenCache>();\n\nasync function fetchServiceToken(issuer: string, clientId: string, clientSecret: string): Promise<string | null> {\n const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString(\"base64\");\n try {\n const res = await fetch(createUrl(issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${credentials}`,\n },\n body: \"grant_type=service_account\",\n cache: \"no-store\",\n });\n if (!res.ok) return null;\n const data = (await res.json()) as { access_token?: string };\n return data.access_token ?? null;\n } catch {\n return null;\n }\n}\n\nasync function getServiceToken(issuer: string, clientId: string, clientSecret: string): Promise<string | null> {\n const key = `${issuer}:${clientId}`;\n const now = Date.now();\n const cached = _serviceTokenCache.get(key);\n if (cached && cached.expiresAt - 30_000 > now) return cached.token;\n const token = await fetchServiceToken(issuer, clientId, clientSecret);\n if (!token) return null;\n _serviceTokenCache.set(key, { token, expiresAt: now + 420_000 });\n return token;\n}\n\ntype ServiceAccountConfig = {\n issuer?: string;\n clientId?: string;\n clientSecret?: string;\n};\n\n/**\n * Returns a service account access token.\n * The issuer is always the Geekapps Auth Server (GEEKAPPS_AUTH_SERVER_URL) —\n * it does not need to be configured. Reads GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID,\n * GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET by default.\n * Any field can be overridden via the optional config argument.\n * Tokens are cached per client and auto-renewed before expiry (~7 min TTL).\n */\nexport function getServiceAccountToken(override: ServiceAccountConfig = {}): Promise<string | null> {\n const issuer = normalizeBaseUrl(\n override.issuer ??\n // Deprecated: kept for backwards compatibility during the auth-server rename.\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n GEEKAPPS_AUTH_SERVER_URL,\n );\n const clientId = override.clientId ?? env(\"GEEKAPPS_SERVICE_ACCOUNT_CLIENT_ID\") ?? \"\";\n const clientSecret = override.clientSecret ?? env(\"GEEKAPPS_SERVICE_ACCOUNT_CLIENT_SECRET\") ?? \"\";\n if (!clientId || !clientSecret) return Promise.resolve(null);\n return getServiceToken(issuer, clientId, clientSecret);\n}\n\nfunction getRemoteJWKS(issuer: string) {\n if (!_remoteJWKSCache.has(issuer)) {\n _remoteJWKSCache.set(issuer, createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)));\n }\n return _remoteJWKSCache.get(issuer)!;\n}\n\nexport async function verifyToken(\n token: string,\n issuer: string,\n): Promise<JWTPayload | null> {\n try {\n const { payload } = await jwtVerify(token, getRemoteJWKS(issuer), {\n algorithms: [\"RS256\"],\n issuer,\n });\n return payload;\n } catch {\n return null;\n }\n}\n\nexport function configureGeekappsAuth(input: GeekappsAuthConfig = {}) {\n const clientId = resolveClientId(input);\n const config: ResolvedGeekappsAuthConfig = {\n ...input,\n issuer: resolveIssuer(input),\n clientId,\n clientSecret: resolveClientSecret(input),\n redirectUri: resolveConfiguredRedirectUri(input),\n afterSignInRedirectTo:\n input.afterSignInRedirectTo ?? input.redirectTo ?? \"/\",\n basePath: normalizeBasePath(input.basePath),\n cookiePrefix: deriveCookiePrefix(clientId),\n };\n\n const cn = getCookieNames(config.cookiePrefix);\n\n // ── Slot helpers ──────────────────────────────────────────────────────────\n\n async function getCount(): Promise<number> {\n const cookieStore = await cookies();\n const raw = cookieStore.get(cn.count)?.value;\n const n = raw ? Number.parseInt(raw, 10) : 0;\n return Number.isNaN(n) ? 0 : n;\n }\n\n async function getActiveIndex(): Promise<number> {\n const cookieStore = await cookies();\n const raw = cookieStore.get(cn.active)?.value;\n if (!raw) return 0;\n const n = Number.parseInt(raw, 10);\n return Number.isNaN(n) ? 0 : n;\n }\n\n function readSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n ): StoredAccount | null {\n const at = cookieStore.get(cn.slotAt(n))?.value;\n if (!at) return null;\n const meta = decodeMeta(cookieStore.get(cn.slotMeta(n))?.value ?? \"\");\n if (!meta) return null;\n return {\n id: meta.id,\n accessToken: at,\n refreshToken: cookieStore.get(cn.slotRt(n))?.value ?? null,\n idToken: null, // not stored in cookies; claims are in meta\n sessionId: meta.sid ?? null,\n user: {\n sub: meta.id,\n name: meta.n ?? null,\n email: meta.e ?? null,\n picture: meta.p ?? null,\n },\n };\n }\n\n function writeSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n account: StoredAccount,\n ) {\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n cookieStore.set(cn.slotMeta(n), encodeMeta(account), opts);\n cookieStore.set(cn.slotAt(n), account.accessToken, opts);\n if (account.refreshToken) cookieStore.set(cn.slotRt(n), account.refreshToken, opts);\n }\n\n function clearSlot(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n n: number,\n ) {\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n cookieStore.set(cn.slotMeta(n), \"\", clear);\n cookieStore.set(cn.slotAt(n), \"\", clear);\n cookieStore.set(cn.slotRt(n), \"\", clear);\n }\n\n // ── Public account helpers ─────────────────────────────────────────────────\n\n async function getAccountsList(): Promise<StoredAccount[]> {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const accounts: StoredAccount[] = [];\n for (let i = 1; i <= count; i++) {\n const slot = readSlot(cookieStore, i);\n if (slot) accounts.push(slot);\n }\n return accounts;\n }\n\n async function getActiveAccount(): Promise<StoredAccount | null> {\n const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);\n if (count === 0) return null;\n if (activeIdx >= 1 && activeIdx <= count) {\n const slot = readSlot(cookieStore, activeIdx);\n if (slot) return slot;\n }\n // Fall back to slot 1\n return readSlot(cookieStore, 1);\n }\n\n async function getAccessToken() {\n // Prefer the token injected by the middleware via request header (same-request refresh)\n try {\n const hdrs = await headers();\n const auth = hdrs.get(\"authorization\");\n if (auth?.startsWith(\"Bearer \")) return auth.slice(7);\n } catch {}\n const active = await getActiveAccount();\n return active?.accessToken ?? null;\n }\n\n async function getRefreshToken() {\n const active = await getActiveAccount();\n return active?.refreshToken ?? null;\n }\n\n async function getSession(): Promise<SessionTokens | null> {\n const active = await getActiveAccount();\n if (!active) return null;\n return {\n accessToken: active.accessToken,\n refreshToken: active.refreshToken,\n idToken: active.idToken,\n sessionId: active.sessionId,\n };\n }\n\n async function getUser(): Promise<StoredAccount[\"user\"] | null> {\n const active = await getActiveAccount();\n return active?.user ?? null;\n }\n\n async function getUserById(userId: string, applicationId?: string): Promise<unknown | null> {\n const qs = new URLSearchParams({ ...(applicationId ? { applicationId } : {}) });\n try {\n return await apiRequest<unknown>(`/users/${userId}?${qs}`);\n } catch {\n return null;\n }\n }\n\n async function getOrganizations(): Promise<{ organizations: unknown[]; currentOrgId: string | null }> {\n const cookieStore = await cookies();\n const currentOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n try {\n const data = await apiRequest<unknown[] | { organizations: unknown[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: unknown[] }).organizations;\n return { organizations, currentOrgId };\n } catch {\n return { organizations: [], currentOrgId };\n }\n }\n\n function clearPublicTokenCookie(\n cookieStore: Awaited<ReturnType<typeof cookies>>,\n ) {\n // Clear legacy _tp cookie if present\n if (cookieStore.get(cn.tokenPub)?.value) {\n cookieStore.set(cn.tokenPub, \"\", { httpOnly: false, maxAge: 0, path: \"/\" });\n }\n }\n\n async function saveAccountToList(tokens: SessionTokens) {\n const account = parseAccountFromJwt(tokens);\n if (!account) return;\n\n const cookieStore = await cookies();\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n const count = await getCount();\n\n // Find existing slot for this account id\n let targetSlot = 0;\n for (let i = 1; i <= count; i++) {\n const existing = readSlot(cookieStore, i);\n if (existing?.id === account.id) {\n targetSlot = i;\n break;\n }\n }\n\n if (targetSlot === 0) {\n if (count >= MAX_ACCOUNTS) return;\n targetSlot = count + 1;\n cookieStore.set(cn.count, String(targetSlot), opts);\n }\n\n writeSlot(cookieStore, targetSlot, account);\n cookieStore.set(cn.active, String(targetSlot), opts);\n clearPublicTokenCookie(cookieStore);\n }\n\n // Alias for backwards compat\n const setSessionCookies = saveAccountToList;\n\n async function switchAccount(accountId: string): Promise<boolean> {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n\n for (let i = 1; i <= count; i++) {\n const slot = readSlot(cookieStore, i);\n if (slot && (slot.id === accountId || slot.sessionId === accountId)) {\n cookieStore.set(cn.active, String(i), opts);\n clearPublicTokenCookie(cookieStore);\n return true;\n }\n }\n return false;\n }\n\n async function removeActiveAccount() {\n const [cookieStore, activeIdx, count] = await Promise.all([cookies(), getActiveIndex(), getCount()]);\n const opts = { ...getCookieOptions(config), maxAge: 60 * 60 * 24 * 30 };\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n\n if (count === 0) return;\n\n const remaining: StoredAccount[] = [];\n for (let i = 1; i <= count; i++) {\n if (i === activeIdx) continue;\n const slot = readSlot(cookieStore, i);\n if (slot) remaining.push(slot);\n }\n\n for (let i = 1; i <= count; i++) {\n clearSlot(cookieStore, i);\n }\n\n if (remaining.length === 0) {\n cookieStore.set(cn.count, \"0\", clear);\n cookieStore.set(cn.active, \"0\", clear);\n await clearPublicTokenCookie(cookieStore);\n return;\n }\n\n // Re-compact remaining accounts into slots 1..n\n for (let i = 0; i < remaining.length; i++) {\n await writeSlot(cookieStore, i + 1, remaining[i]!);\n }\n cookieStore.set(cn.count, String(remaining.length), opts);\n cookieStore.set(cn.active, \"1\", opts);\n clearPublicTokenCookie(cookieStore);\n }\n\n async function clearAllSessionCookies() {\n const [cookieStore, count] = await Promise.all([cookies(), getCount()]);\n const clear = { ...getCookieOptions(config), maxAge: 0 };\n for (let i = 1; i <= count; i++) {\n clearSlot(cookieStore, i);\n }\n cookieStore.set(cn.count, \"0\", clear);\n cookieStore.set(cn.active, \"0\", clear);\n clearPublicTokenCookie(cookieStore);\n }\n\n async function tryRefreshTokens(): Promise<string | null> {\n const refreshToken = await getRefreshToken();\n if (!refreshToken || !config.clientId) return null;\n\n const basicAuth = Buffer.from(`${config.clientId}:${config.clientSecret ?? \"\"}`).toString(\"base64\");\n\n const response = await fetch(createUrl(config.issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n }),\n cache: \"no-store\",\n });\n\n if (!response.ok) return null;\n\n const data = (await response.json().catch(() => null)) as TokenLikeResponse | null;\n if (!data) return null;\n\n const tokens = tokenFromResponse(data);\n if (!tokens.accessToken) return null;\n\n await saveAccountToList(tokens);\n return tokens.accessToken;\n }\n\n async function apiRequest<T>(path: string, init: RequestInit = {}): Promise<T> {\n const accessToken = await getAccessToken();\n const requestHeaders = new Headers(init.headers);\n\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (init.body !== undefined && !requestHeaders.has(\"content-type\")) {\n requestHeaders.set(\"content-type\", \"application/json\");\n }\n\n const url = new URL(createUrl(config.issuer, path));\n applyClientCredentials(url, config);\n\n const response = await fetch(url, {\n ...init,\n headers: requestHeaders,\n cache: \"no-store\",\n });\n\n if (response.status === 401) {\n const newAccessToken = await tryRefreshTokens();\n if (newAccessToken) {\n const retryHeaders = new Headers(init.headers);\n retryHeaders.set(\"authorization\", `Bearer ${newAccessToken}`);\n if (init.body !== undefined && !retryHeaders.has(\"content-type\")) {\n retryHeaders.set(\"content-type\", \"application/json\");\n }\n const retryUrl = new URL(createUrl(config.issuer, path));\n applyClientCredentials(retryUrl, config);\n const retryResponse = await fetch(retryUrl, {\n ...init,\n headers: retryHeaders,\n cache: \"no-store\",\n });\n if (!retryResponse.ok) {\n const error = await retryResponse.json().catch(() => null);\n throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${retryResponse.status}`);\n }\n if (retryResponse.status === 204) return null as T;\n return (await retryResponse.json()) as T;\n }\n }\n\n if (!response.ok) {\n const error = await response.json().catch(() => null);\n throw new Error(error?.error_description ?? error?.error ?? error?.message ?? `Auth API error ${response.status}`);\n }\n\n if (response.status === 204) return null as T;\n return (await response.json()) as T;\n }\n\n function createApiClient(): ServerAuthApiClient {\n return {\n get: (path) => apiRequest(path),\n post: (path, body) => apiRequest(path, { method: \"POST\", body: body === undefined ? undefined : JSON.stringify(body) }),\n patch: (path, body) => apiRequest(path, { method: \"PATCH\", body: body === undefined ? undefined : JSON.stringify(body) }),\n put: (path, body) => apiRequest(path, { method: \"PUT\", body: body === undefined ? undefined : JSON.stringify(body) }),\n delete: (path) => apiRequest(path, { method: \"DELETE\" }),\n };\n }\n\n async function requireUser(options: { redirectTo?: string } = {}) {\n const accessToken = await getAccessToken();\n if (!accessToken) {\n redirect(options.redirectTo ?? \"/\");\n }\n return accessToken;\n }\n\n function escapeHtml(str: string): string {\n return str.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n }\n\n function sanitizeRedirectTo(value: string | null | undefined): string {\n const raw = value ?? \"/\";\n // Never redirect back into the auth basePath — that would loop or replay a stale code.\n if (raw.startsWith(config.basePath)) return \"/\";\n // Only allow relative paths to prevent open-redirect.\n if (raw.startsWith(\"/\") && !raw.startsWith(\"//\")) return raw;\n return \"/\";\n }\n\n async function createAuthorizeRedirect(request: NextRequest, mode: \"login\" | \"register\" | \"add-account\" | \"google\") {\n const redirectTo = sanitizeRedirectTo(request.nextUrl.searchParams.get(\"redirectTo\") ?? config.afterSignInRedirectTo);\n const flowId = request.nextUrl.searchParams.get(\"flow_id\");\n const url = new URL(createUrl(config.issuer, mode === \"google\" ? \"/auth/google\" : \"/authorize\"));\n\n if (!config.clientId) {\n return NextResponse.json(\n { error: \"missing_client_id\", message: \"Missing GEEKAPPS_AUTH_CLIENT_ID or clientId.\" },\n { status: 500 },\n );\n }\n\n url.searchParams.set(\"client_id\", config.clientId);\n url.searchParams.set(\"redirect_uri\", resolveRedirectUri(config, request));\n url.searchParams.set(\"response_type\", \"code\");\n const scopes = config.scopes ?? [\"openid\", \"profile\", \"email\"];\n if (!scopes.includes(\"offline_access\")) scopes.push(\"offline_access\");\n url.searchParams.set(\"scope\", scopes.join(\" \"));\n url.searchParams.set(\"state\", Buffer.from(JSON.stringify({ redirectTo, mode })).toString(\"base64url\"));\n if (mode === \"register\") url.searchParams.set(\"screen_hint\", \"signup\");\n if (mode === \"add-account\") {\n // Adding an account always needs a fresh login, regardless of any existing session.\n url.searchParams.set(\"prompt\", \"login\");\n } else {\n // `prompt` (login | select_account | none | consent) is forwarded as-is to\n // the IDP — see OidcPrompt in client.tsx for what each value does.\n const prompt = request.nextUrl.searchParams.get(\"prompt\");\n // Legacy fallback: silentLogin=false is equivalent to prompt=login.\n const silentLogin = request.nextUrl.searchParams.get(\"silentLogin\");\n const resolvedPrompt = prompt ?? (silentLogin === \"false\" ? \"login\" : null);\n if (resolvedPrompt) url.searchParams.set(\"prompt\", resolvedPrompt);\n }\n if (flowId) url.searchParams.set(\"flow_id\", flowId);\n\n return NextResponse.redirect(url);\n }\n\n async function exchangeCode(request: NextRequest) {\n const code = request.nextUrl.searchParams.get(\"code\");\n const state = request.nextUrl.searchParams.get(\"state\");\n\n const origin = requestOrigin(request);\n\n if (!code) {\n const err = request.nextUrl.searchParams.get(\"error\") ?? \"missing_code\";\n const desc = request.nextUrl.searchParams.get(\"error_description\") ?? request.nextUrl.searchParams.get(\"error_message\") ?? \"No authorization code received\";\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${encodeURIComponent(err)}&error_description=${encodeURIComponent(desc)}`, origin));\n }\n\n if (!config.clientId) {\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=missing_client_id&error_description=Auth+client+is+not+configured`, origin));\n }\n\n const basicAuth = Buffer.from(\n `${config.clientId}:${config.clientSecret ?? \"\"}`,\n ).toString(\"base64\");\n\n const response = await fetch(createUrl(config.issuer, \"/auth/token\"), {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: resolveRedirectUri(config, request),\n }),\n cache: \"no-store\",\n });\n\n const data = (await response.json().catch(() => ({}))) as TokenLikeResponse & { error?: string; error_description?: string };\n if (!response.ok) {\n const error = encodeURIComponent((data.error as string | undefined) ?? \"token_exchange_failed\");\n const desc = encodeURIComponent((data.error_description as string | undefined) ?? `Token exchange failed with status ${response.status}`);\n return NextResponse.redirect(new URL(`${config.basePath}/callback?error=${error}&error_description=${desc}`, origin));\n }\n\n await saveAccountToList(tokenFromResponse(data));\n\n let redirectTo = sanitizeRedirectTo(config.afterSignInRedirectTo);\n if (state) {\n try {\n const parsed = JSON.parse(Buffer.from(state, \"base64url\").toString(\"utf8\")) as { redirectTo?: string };\n redirectTo = sanitizeRedirectTo(parsed.redirectTo);\n } catch {\n // ignore malformed state\n }\n }\n\n return NextResponse.redirect(new URL(redirectTo, origin));\n }\n\n function mapLocalPathToApiPath(route: string) {\n if (route === \"/me\") return \"/auth/me\";\n if (route === \"/accounts/switch\") return \"/auth/accounts/switch\";\n if (route === \"/organizations/active\") return \"/auth/organizations/active\";\n if (route === \"/permissions/check\") return \"/auth/permissions/check\";\n if (route === \"/login/password\") return \"/auth/login/password\";\n if (route === \"/register/password\") return \"/auth/register/password\";\n if (route === \"/passwordless/link\") return \"/auth/passwordless/link\";\n return route;\n }\n\n async function handleAccountsLocal(): Promise<NextResponse> {\n const [accounts, activeIdx] = await Promise.all([getAccountsList(), getActiveIndex()]);\n\n return NextResponse.json({\n accounts: accounts.map((a, i) => ({\n id: a.id,\n sessionId: a.sessionId ?? a.id,\n isActive: i + 1 === activeIdx,\n user: {\n id: a.user.sub,\n name: a.user.name ?? null,\n email: a.user.email ?? null,\n imageUrl: a.user.picture ?? null,\n phone: a.user.phone ?? null,\n },\n })),\n });\n }\n\n async function handleOrganizationsLocal(): Promise<NextResponse> {\n const cookieStore = await cookies();\n const activeOrgId = cookieStore.get(cn.activeOrganizationId)?.value ?? null;\n try {\n const data = await apiRequest<unknown[]|{ organizations: unknown[] }>(\"/organizations\");\n const organizations = Array.isArray(data) ? data : (data as { organizations: unknown[] }).organizations;\n return NextResponse.json({ organizations, currentOrgId: activeOrgId });\n } catch {\n return NextResponse.json({ organizations: [], currentOrgId: activeOrgId });\n }\n }\n\n async function handleSwitchAccountLocal(request: NextRequest): Promise<NextResponse> {\n const body = await readBody(request);\n const targetId =\n body && typeof body === \"object\"\n ? String(\n (body as Record<string, unknown>).accountId ??\n (body as Record<string, unknown>).sessionId ??\n \"\",\n )\n : \"\";\n\n if (!targetId) {\n return NextResponse.json({ error: \"accountId required\" }, { status: 400 });\n }\n\n const switched = await switchAccount(targetId);\n if (!switched) {\n return NextResponse.json({ error: \"Account not found\" }, { status: 404 });\n }\n\n return NextResponse.json({ ok: true });\n }\n\n async function proxy(request: NextRequest, route: string) {\n const accessToken = await getAccessToken();\n const body = [\"GET\", \"HEAD\"].includes(request.method) ? undefined : await readBody(request);\n const url = new URL(appendSearchParams(createUrl(config.issuer, mapLocalPathToApiPath(route)), request));\n applyClientCredentials(url, config);\n const requestHeaders = new Headers();\n\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (body !== undefined) requestHeaders.set(\"content-type\", \"application/json\");\n\n const response = await fetch(url, {\n method: request.method,\n headers: requestHeaders,\n body: body === undefined ? undefined : typeof body === \"string\" ? body : JSON.stringify(body),\n cache: \"no-store\",\n });\n\n const data = isJsonResponse(response) ? ((await response.json().catch(() => null)) as TokenLikeResponse | null) : null;\n if (data) {\n const tokens = tokenFromResponse(data);\n if (tokens.accessToken || tokens.refreshToken || tokens.idToken || tokens.sessionId) {\n await saveAccountToList(tokens);\n }\n return NextResponse.json(data, { status: response.status });\n }\n\n return responseFromApi(response);\n }\n\n async function handle(request: NextRequest) {\n const route = routeFromRequest(request, config.basePath);\n\n if (request.method === \"GET\" && route === \"/sign-in\") return createAuthorizeRedirect(request, \"login\");\n if (request.method === \"GET\" && route === \"/sign-up\") return createAuthorizeRedirect(request, \"register\");\n if (request.method === \"GET\" && route === \"/add-account\") return createAuthorizeRedirect(request, \"add-account\");\n if (request.method === \"GET\" && route === \"/login/google\") return createAuthorizeRedirect(request, \"google\");\n if (request.method === \"GET\" && route === \"/callback\") {\n const error = request.nextUrl.searchParams.get(\"error\");\n if (error) {\n const description = request.nextUrl.searchParams.get(\"error_description\") ?? request.nextUrl.searchParams.get(\"error_message\") ?? \"An authentication error occurred.\";\n return new NextResponse(\n `<!doctype html><html><head><meta charset=\"utf-8\"><title>Authentication Error</title></head><body style=\"font-family:sans-serif;max-width:480px;margin:80px auto;padding:0 16px\"><h2>Authentication Error</h2><p><strong>${escapeHtml(error)}</strong></p><p>${escapeHtml(description)}</p><a href=\"/\">Go back</a></body></html>`,\n { status: 400, headers: { \"content-type\": \"text/html; charset=utf-8\" } },\n );\n }\n return exchangeCode(request);\n }\n if (request.method === \"GET\" && route === \"/token\") {\n const accessToken = await getAccessToken();\n if (!accessToken) return NextResponse.json({ error: \"missing_token\" }, { status: 401 });\n return NextResponse.json({ accessToken });\n }\n\n\n if (request.method === \"GET\" && route === \"/accounts\") return handleAccountsLocal();\n if (request.method === \"POST\" && route === \"/accounts/switch\") return handleSwitchAccountLocal(request);\n if (request.method === \"GET\" && route === \"/organizations\") return handleOrganizationsLocal();\n\n if (request.method === \"POST\" && route === \"/organizations\") {\n const accessToken = await getAccessToken();\n const body = (await readBody(request)) as Record<string, unknown> | undefined;\n if (body && accessToken && !body.applicationId) {\n const claims = decodeJwtPayload(accessToken);\n const applicationId =\n (typeof claims?.application_id === \"string\" ? claims.application_id : null) ??\n (typeof claims?.client_id === \"string\" ? claims.client_id : null) ??\n (typeof claims?.applicationId === \"string\" ? claims.applicationId : null);\n if (applicationId) body.applicationId = applicationId;\n }\n const orgUrl = new URL(createUrl(config.issuer, \"/organizations\"));\n applyClientCredentials(orgUrl, config);\n const requestHeaders = new Headers();\n if (accessToken) requestHeaders.set(\"authorization\", `Bearer ${accessToken}`);\n if (body !== undefined) requestHeaders.set(\"content-type\", \"application/json\");\n const response = await fetch(orgUrl, {\n method: \"POST\",\n headers: requestHeaders,\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n return responseFromApi(response);\n }\n\n if (request.method === \"POST\" && route === \"/sign-out\") {\n await apiRequest(\"/auth/sign-out\", { method: \"POST\" }).catch(() => null);\n await removeActiveAccount();\n return NextResponse.json({ ok: true });\n }\n\n if (request.method === \"POST\" && route === \"/sign-out-all\") {\n await apiRequest(\"/auth/sign-out-all\", { method: \"POST\" }).catch(() => null);\n await clearAllSessionCookies();\n return NextResponse.json({ ok: true });\n }\n\n if (request.method === \"POST\" && route === \"/organizations/active\") {\n const body = await readBody(request);\n const organizationId =\n body && typeof body === \"object\" && \"organizationId\" in body\n ? (body as Record<string, unknown>).organizationId\n : null;\n const response = NextResponse.json({ ok: true });\n if (typeof organizationId === \"string\") {\n const cookieOpts = {\n httpOnly: true,\n sameSite: \"lax\" as const,\n path: \"/\",\n secure: process.env.NODE_ENV === \"production\",\n maxAge: 60 * 60 * 24 * 7,\n };\n response.cookies.set(cn.activeOrganizationId, organizationId, cookieOpts);\n }\n return response;\n }\n\n return proxy(request, route);\n }\n\n return {\n handlers: {\n GET: handle,\n POST: handle,\n PATCH: handle,\n DELETE: handle,\n },\n GetSession: getSession,\n GetUser: getUser,\n GetUserById: getUserById,\n GetOrganizations: getOrganizations,\n SaveSession: saveAccountToList,\n GetAccessToken: getAccessToken,\n GetRefreshToken: getRefreshToken,\n IsAuthenticated: async () => (await getAccessToken()) !== null,\n TryRefreshTokens: tryRefreshTokens,\n RequireUser: requireUser,\n CreateAuthApiClient: createApiClient,\n cookiePrefix: config.cookiePrefix,\n VerifyToken: (token: string) => verifyToken(token, config.issuer),\n SignInUrl: async (redirectTo?: string) => {\n const configured = env(\"GEEKAPPS_APP_URL\");\n if (!configured) throw new Error(\"Cannot determine app origin. Set GEEKAPPS_APP_URL.\");\n const origin = configured.replace(/\\/$/, \"\");\n const url = new URL(`${origin}${config.basePath}/sign-in`);\n if (redirectTo) url.searchParams.set(\"redirectTo\", redirectTo);\n return url.toString();\n },\n };\n}\n\nexport function handlers(options?: GeekappsAuthConfig) {\n return configureGeekappsAuth(options).handlers;\n}\n\n/**\n * Returns true if the current request has a valid access token in cookies.\n * Safe to call in Server Components, Server Actions, and Route Handlers.\n */\nexport async function isAuthenticated(options?: GeekappsAuthConfig): Promise<boolean> {\n const { GetAccessToken } = configureGeekappsAuth(options);\n return (await GetAccessToken()) !== null;\n}\n"],"mappings":"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","names":["cookies","headers","headers","cookies"]}
|
package/dist/client.d.ts
CHANGED
|
@@ -74,6 +74,108 @@ type OrganizationMember = {
|
|
|
74
74
|
user: User;
|
|
75
75
|
roleAssignments?: RoleAssignment[];
|
|
76
76
|
};
|
|
77
|
+
type OrganizationInvite = {
|
|
78
|
+
id: string;
|
|
79
|
+
organizationId: string;
|
|
80
|
+
email: string;
|
|
81
|
+
roleId?: string | null;
|
|
82
|
+
status: "pending" | "accepted" | "expired" | "revoked";
|
|
83
|
+
expiresAt: string | Date;
|
|
84
|
+
createdAt?: string | Date;
|
|
85
|
+
};
|
|
86
|
+
type MemberInviteInput = {
|
|
87
|
+
email: string;
|
|
88
|
+
roleId?: string;
|
|
89
|
+
/**
|
|
90
|
+
* URI pra onde o link do e-mail manda o usuário — precisa estar cadastrada
|
|
91
|
+
* em Application.redirectUris (mesma validação do /authorize). Este app
|
|
92
|
+
* deve expor uma rota nessa URI que trata o `?invite_token=` anexado (ver
|
|
93
|
+
* useJoinRequests/inviteApi.preview|accept|reject abaixo).
|
|
94
|
+
*/
|
|
95
|
+
redirectUri: string;
|
|
96
|
+
emailTitle?: string;
|
|
97
|
+
emailSubtitle?: string;
|
|
98
|
+
emailMessage?: string;
|
|
99
|
+
};
|
|
100
|
+
type AddMemberInput = {
|
|
101
|
+
userId: string;
|
|
102
|
+
roleId?: string;
|
|
103
|
+
};
|
|
104
|
+
type UpdateMemberInput = {
|
|
105
|
+
status?: "active" | "inactive" | "suspended";
|
|
106
|
+
roleId?: string | null;
|
|
107
|
+
};
|
|
108
|
+
/** @deprecated use UpdateMemberInput */
|
|
109
|
+
type UpdateMemberRoleInput = {
|
|
110
|
+
roleId: string;
|
|
111
|
+
};
|
|
112
|
+
type PublicOrganization = {
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
slug: string;
|
|
116
|
+
description: string | null;
|
|
117
|
+
imageUrl: string | null;
|
|
118
|
+
createdAt: string | Date;
|
|
119
|
+
};
|
|
120
|
+
type OrganizationInvitePreview = {
|
|
121
|
+
alreadyAccepted: true;
|
|
122
|
+
organizationName: string;
|
|
123
|
+
applicationName: string;
|
|
124
|
+
applicationImageUrl: string | null;
|
|
125
|
+
applicationImageFileId: string | null;
|
|
126
|
+
organizationImageUrl: string | null;
|
|
127
|
+
organizationImageFileId: string | null;
|
|
128
|
+
websiteUrl: string | null;
|
|
129
|
+
} | {
|
|
130
|
+
alreadyAccepted?: false;
|
|
131
|
+
email: string;
|
|
132
|
+
organizationName: string;
|
|
133
|
+
applicationName: string;
|
|
134
|
+
applicationId: string;
|
|
135
|
+
applicationImageUrl: string | null;
|
|
136
|
+
applicationImageFileId: string | null;
|
|
137
|
+
organizationImageUrl: string | null;
|
|
138
|
+
organizationImageFileId: string | null;
|
|
139
|
+
websiteUrl: string | null;
|
|
140
|
+
redirectUri: string | null;
|
|
141
|
+
appStoreUrl: string | null;
|
|
142
|
+
playStoreUrl: string | null;
|
|
143
|
+
currentAccount: {
|
|
144
|
+
email: string;
|
|
145
|
+
name: string | null;
|
|
146
|
+
} | null;
|
|
147
|
+
};
|
|
148
|
+
type AcceptInviteResult = {
|
|
149
|
+
success: true;
|
|
150
|
+
organizationName: string;
|
|
151
|
+
websiteUrl: string | null;
|
|
152
|
+
} | {
|
|
153
|
+
success: false;
|
|
154
|
+
wrongAccount: true;
|
|
155
|
+
currentEmail: string;
|
|
156
|
+
invitedEmail: string;
|
|
157
|
+
} | {
|
|
158
|
+
success: false;
|
|
159
|
+
wrongAccount: false;
|
|
160
|
+
loginRequired: true;
|
|
161
|
+
} | {
|
|
162
|
+
success: false;
|
|
163
|
+
wrongAccount: false;
|
|
164
|
+
loginRequired: false;
|
|
165
|
+
error: string;
|
|
166
|
+
};
|
|
167
|
+
type JoinRequestStatus = "pending" | "approved" | "rejected" | "cancelled";
|
|
168
|
+
type OrganizationJoinRequest = {
|
|
169
|
+
id: string;
|
|
170
|
+
organizationId: string;
|
|
171
|
+
userId: string;
|
|
172
|
+
status: JoinRequestStatus;
|
|
173
|
+
message: string | null;
|
|
174
|
+
reviewedByUserId: string | null;
|
|
175
|
+
reviewedAt: string | null;
|
|
176
|
+
createdAt: string | Date;
|
|
177
|
+
updatedAt: string | Date;
|
|
178
|
+
};
|
|
77
179
|
type OrganizationOwnedApplication = {
|
|
78
180
|
id: string;
|
|
79
181
|
name: string;
|
|
@@ -96,6 +198,8 @@ type Organization = {
|
|
|
96
198
|
imageUrl: string | null;
|
|
97
199
|
faviconUrl: string | null;
|
|
98
200
|
status: OrganizationStatus;
|
|
201
|
+
/** "private" (padrão) nunca sai em listPublicOrganizations(); "public" sai, e aceita join requests. */
|
|
202
|
+
visibility?: "public" | "private";
|
|
99
203
|
members?: OrganizationMember[];
|
|
100
204
|
ownedApplications?: OrganizationOwnedApplication[];
|
|
101
205
|
applicationLinks?: Array<{
|
|
@@ -180,10 +284,43 @@ type PermissionUpdateInput = {
|
|
|
180
284
|
category?: string | null;
|
|
181
285
|
isActive?: boolean;
|
|
182
286
|
};
|
|
287
|
+
/**
|
|
288
|
+
* OIDC `prompt` values understood by the Geekapps Auth Server. These control
|
|
289
|
+
* whether — and how — the IDP shows UI during `signIn()`/`signUp()`/`addAccount()`.
|
|
290
|
+
*
|
|
291
|
+
* - `"login"` — Forces the login screen, even if the user already has an active
|
|
292
|
+
* session. Use this for a fresh login, unrelated to whatever session already
|
|
293
|
+
* exists (equivalent to the old `silentLogin: false`).
|
|
294
|
+
* - `"select_account"` — Shows the account picker so the user can choose among
|
|
295
|
+
* their connected accounts, or add a new one. Use this when you want to let
|
|
296
|
+
* the user switch accounts explicitly.
|
|
297
|
+
* - `"none"` — Never shows any UI. The IDP authenticates silently if there is
|
|
298
|
+
* exactly one active session (and no other interaction — like consent or a
|
|
299
|
+
* security check — is pending); otherwise it fails immediately with an OIDC
|
|
300
|
+
* error (`login_required`, `account_selection_required`, `consent_required`,
|
|
301
|
+
* or `interaction_required`) instead of prompting the user. Use this for
|
|
302
|
+
* silent-auth checks (e.g. "is the user still logged in?") where you don't
|
|
303
|
+
* want to interrupt them with a screen.
|
|
304
|
+
* - `"consent"` — Forces the consent screen even if the user already granted
|
|
305
|
+
* the requested scopes before.
|
|
306
|
+
*
|
|
307
|
+
* Omitting `prompt` entirely runs the IDP's default flow: reuse a single
|
|
308
|
+
* existing session silently, ask the user to pick when there are multiple
|
|
309
|
+
* connected accounts, or show the login screen when there's none.
|
|
310
|
+
*/
|
|
311
|
+
type OidcPrompt = "login" | "select_account" | "none" | "consent";
|
|
183
312
|
type SignInOptions = {
|
|
184
313
|
redirectTo?: string;
|
|
185
314
|
authorizationFlowId?: string;
|
|
186
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Controls whether the IDP shows UI, and which screen, during sign-in. See
|
|
317
|
+
* {@link OidcPrompt} for what each value does. Omit it to run the default flow.
|
|
318
|
+
*/
|
|
319
|
+
prompt?: OidcPrompt;
|
|
320
|
+
/**
|
|
321
|
+
* @deprecated Use `prompt: "login"` instead. Kept for backwards compatibility —
|
|
322
|
+
* `silentLogin: false` is equivalent to `prompt: "login"`.
|
|
323
|
+
*/
|
|
187
324
|
silentLogin?: boolean;
|
|
188
325
|
};
|
|
189
326
|
type SignInInput = SignInOptions | MouseEvent<HTMLElement>;
|
|
@@ -289,6 +426,27 @@ type GeekappsAuthContextValue = {
|
|
|
289
426
|
updateOrganization: (id: string, input: OrganizationUpdateInput) => Promise<AuthResult<Organization>>;
|
|
290
427
|
deleteOrganization: (id: string) => Promise<AuthResult<null>>;
|
|
291
428
|
hasPermission: (options: PermissionCheckOptions) => Promise<boolean>;
|
|
429
|
+
/** Organizações públicas e ativas deste app — nunca inclui privadas, não exige membership. Separado de `organizations` (só as do próprio usuário). */
|
|
430
|
+
listPublicOrganizations: () => Promise<PublicOrganization[]>;
|
|
431
|
+
requestToJoin: (organizationId: string, input?: {
|
|
432
|
+
message?: string | null;
|
|
433
|
+
}) => Promise<AuthResult<OrganizationJoinRequest>>;
|
|
434
|
+
getMyJoinRequest: (organizationId: string) => Promise<OrganizationJoinRequest | null>;
|
|
435
|
+
cancelJoinRequest: (organizationId: string) => Promise<AuthResult<null>>;
|
|
436
|
+
listJoinRequests: (organizationId: string) => Promise<OrganizationJoinRequest[]>;
|
|
437
|
+
approveJoinRequest: (organizationId: string, requestId: string) => Promise<AuthResult<OrganizationJoinRequest>>;
|
|
438
|
+
rejectJoinRequest: (organizationId: string, requestId: string) => Promise<AuthResult<OrganizationJoinRequest>>;
|
|
439
|
+
/** Preview do convite por token — funciona com ou sem sessão (currentAccount vem null se deslogado). */
|
|
440
|
+
getInvitePreview: (token: string) => Promise<OrganizationInvitePreview>;
|
|
441
|
+
/** Aceita o convite. loginRequired:true significa que signIn() precisa rodar antes. */
|
|
442
|
+
acceptInvite: (token: string) => Promise<AcceptInviteResult>;
|
|
443
|
+
/** Recusa o convite — não exige estar logado. */
|
|
444
|
+
rejectInvite: (token: string) => Promise<{
|
|
445
|
+
success: true;
|
|
446
|
+
} | {
|
|
447
|
+
success: false;
|
|
448
|
+
error: string;
|
|
449
|
+
}>;
|
|
292
450
|
listUsers: (params?: {
|
|
293
451
|
organizationId?: string | null;
|
|
294
452
|
}) => Promise<User[]>;
|
|
@@ -320,6 +478,16 @@ type GeekappsAuthContextValue = {
|
|
|
320
478
|
organizationId?: string | null;
|
|
321
479
|
}) => Promise<UserSession[]>;
|
|
322
480
|
revokeUserSession: (sessionId: string) => Promise<AuthResult<null>>;
|
|
481
|
+
listMembers: (organizationId: string) => Promise<OrganizationMember[]>;
|
|
482
|
+
getMember: (organizationId: string, memberId: string) => Promise<OrganizationMember>;
|
|
483
|
+
addMember: (organizationId: string, input: AddMemberInput) => Promise<AuthResult<OrganizationMember>>;
|
|
484
|
+
updateMember: (organizationId: string, memberId: string, input: UpdateMemberInput) => Promise<AuthResult<OrganizationMember>>;
|
|
485
|
+
/** @deprecated use updateMember */
|
|
486
|
+
updateMemberRole: (organizationId: string, memberId: string, input: UpdateMemberRoleInput) => Promise<AuthResult<OrganizationMember>>;
|
|
487
|
+
removeMember: (organizationId: string, memberId: string) => Promise<AuthResult<null>>;
|
|
488
|
+
inviteMember: (organizationId: string, input: MemberInviteInput) => Promise<AuthResult<OrganizationInvite>>;
|
|
489
|
+
listInvites: (organizationId: string) => Promise<OrganizationInvite[]>;
|
|
490
|
+
revokeInvite: (organizationId: string, inviteId: string) => Promise<AuthResult<null>>;
|
|
323
491
|
};
|
|
324
492
|
type UseOrgResult = Pick<GeekappsAuthContextValue, "organizations" | "currentOrg" | "currentOrgId" | "isLoadingOrganizations" | "refreshOrganizations" | "setCurrentOrg" | "createOrganization" | "updateOrganization" | "deleteOrganization" | "hasPermission">;
|
|
325
493
|
type UseAccountsResult = Pick<GeekappsAuthContextValue, "accounts" | "addAccount" | "isLoadingAccounts" | "refreshAccounts" | "switchAccount">;
|
|
@@ -327,6 +495,10 @@ type UseUsersResult = Pick<GeekappsAuthContextValue, "listUsers" | "getUser" | "
|
|
|
327
495
|
type UseRolesResult = Pick<GeekappsAuthContextValue, "listRoles" | "getRole" | "createRole" | "updateRole" | "deleteRole">;
|
|
328
496
|
type UsePermissionsResult = Pick<GeekappsAuthContextValue, "listPermissions" | "createPermission" | "updatePermission" | "deletePermission">;
|
|
329
497
|
type UseSessionsResult = Pick<GeekappsAuthContextValue, "listUserSessions" | "revokeUserSession">;
|
|
498
|
+
type UseMembersResult = Pick<GeekappsAuthContextValue, "listMembers" | "getMember" | "addMember" | "updateMember" | "updateMemberRole" | "removeMember" | "inviteMember" | "listInvites" | "revokeInvite">;
|
|
499
|
+
type UsePublicOrganizationsResult = Pick<GeekappsAuthContextValue, "listPublicOrganizations">;
|
|
500
|
+
type UseJoinRequestsResult = Pick<GeekappsAuthContextValue, "requestToJoin" | "getMyJoinRequest" | "cancelJoinRequest" | "listJoinRequests" | "approveJoinRequest" | "rejectJoinRequest">;
|
|
501
|
+
type UseInvitesResult = Pick<GeekappsAuthContextValue, "getInvitePreview" | "acceptInvite" | "rejectInvite">;
|
|
330
502
|
declare function GeekappsAuthProvider({ children, basePath, initialUser, initialOrganizations, initialCurrentOrgId, }: GeekappsAuthProviderProps): ReactElement;
|
|
331
503
|
declare function useAuth(): GeekappsAuthContextValue;
|
|
332
504
|
declare function useOrg(): UseOrgResult;
|
|
@@ -335,6 +507,28 @@ declare function useUsers(): UseUsersResult;
|
|
|
335
507
|
declare function useRoles(): UseRolesResult;
|
|
336
508
|
declare function usePermissions(): UsePermissionsResult;
|
|
337
509
|
declare function useSessions(): UseSessionsResult;
|
|
510
|
+
declare function useMembers(): UseMembersResult;
|
|
511
|
+
/**
|
|
512
|
+
* Descoberta de organizações PÚBLICAS do app — bem separado de `useOrg()`,
|
|
513
|
+
* que lida com as organizações do próprio usuário (as que ele já é
|
|
514
|
+
* membro/dono). `usePublicOrganizations()` nunca retorna organizações
|
|
515
|
+
* privadas, e não exige que o usuário já pertença a elas.
|
|
516
|
+
*/
|
|
517
|
+
declare function usePublicOrganizations(): UsePublicOrganizationsResult;
|
|
518
|
+
/**
|
|
519
|
+
* Solicitações de entrada em organizações públicas: um usuário pede pra
|
|
520
|
+
* entrar (requestToJoin) e um admin da organização aprova/rejeita
|
|
521
|
+
* (approveJoinRequest/rejectJoinRequest, exigem permissão org:members:add).
|
|
522
|
+
*/
|
|
523
|
+
declare function useJoinRequests(): UseJoinRequestsResult;
|
|
524
|
+
/**
|
|
525
|
+
* Aceite/recusa de convite por email — chamadas a partir do ?invite_token=
|
|
526
|
+
* que este app recebe na sua própria redirectUri (a que foi passada em
|
|
527
|
+
* useMembers().inviteMember). getInvitePreview funciona sem sessão
|
|
528
|
+
* (currentAccount vem null); rejectInvite não exige login; acceptInvite
|
|
529
|
+
* retorna loginRequired:true se não houver sessão.
|
|
530
|
+
*/
|
|
531
|
+
declare function useInvites(): UseInvitesResult;
|
|
338
532
|
type ServiceAccount = {
|
|
339
533
|
id: string;
|
|
340
534
|
membershipId: string;
|
|
@@ -379,4 +573,4 @@ type UseServiceAccountResult = {
|
|
|
379
573
|
};
|
|
380
574
|
declare function useServiceAccount(): UseServiceAccountResult;
|
|
381
575
|
|
|
382
|
-
export { type AuthResult, type CallbackUrlOptions, type ConnectedAccount, type CreateServiceAccountInput, type GeekappsAuthContextValue, GeekappsAuthProvider, type GeekappsAuthProviderProps, type LoginResponse, type OAuthClientType, type Organization, type OrganizationInput, type OrganizationMember, type OrganizationOwnedApplication, type OrganizationStatus, type OrganizationUpdateInput, type Permission, type PermissionCheckOptions, type PermissionInput, type PermissionUpdateInput, type RegisterResponse, type Role, type RoleAssignment, type RoleInput, type RolePermission, type RoleUpdateInput, type ServiceAccount, type ServiceAccountWithSecret, type SessionApplication, type SignInInput, type SignInOptions, type UseAccountsResult, type UseOrgResult, type UsePermissionsResult, type UseRolesResult, type UseServiceAccountResult, type UseSessionsResult, type UseUsersResult, type User, type UserInput, type UserSession, type UserStatus, loginWithPassword, registerWithPassword, requestPasswordlessLink, startGoogleLogin, useAccounts, useAuth, useOrg, usePermissions, useRoles, useServiceAccount, useSessions, useUsers };
|
|
576
|
+
export { type AcceptInviteResult, type AddMemberInput, type AuthResult, type CallbackUrlOptions, type ConnectedAccount, type CreateServiceAccountInput, type GeekappsAuthContextValue, GeekappsAuthProvider, type GeekappsAuthProviderProps, type JoinRequestStatus, type LoginResponse, type MemberInviteInput, type OAuthClientType, type OidcPrompt, type Organization, type OrganizationInput, type OrganizationInvite, type OrganizationInvitePreview, type OrganizationJoinRequest, type OrganizationMember, type OrganizationOwnedApplication, type OrganizationStatus, type OrganizationUpdateInput, type Permission, type PermissionCheckOptions, type PermissionInput, type PermissionUpdateInput, type PublicOrganization, type RegisterResponse, type Role, type RoleAssignment, type RoleInput, type RolePermission, type RoleUpdateInput, type ServiceAccount, type ServiceAccountWithSecret, type SessionApplication, type SignInInput, type SignInOptions, type UpdateMemberInput, type UpdateMemberRoleInput, type UseAccountsResult, type UseInvitesResult, type UseJoinRequestsResult, type UseMembersResult, type UseOrgResult, type UsePermissionsResult, type UsePublicOrganizationsResult, type UseRolesResult, type UseServiceAccountResult, type UseSessionsResult, type UseUsersResult, type User, type UserInput, type UserSession, type UserStatus, loginWithPassword, registerWithPassword, requestPasswordlessLink, startGoogleLogin, useAccounts, useAuth, useInvites, useJoinRequests, useMembers, useOrg, usePermissions, usePublicOrganizations, useRoles, useServiceAccount, useSessions, useUsers };
|