@geekapps/auth-nextjs 0.22.0 → 0.24.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 +250 -210
- package/dist/actions.js +1 -10
- package/dist/actions.js.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/connections.d.ts +101 -9
- package/dist/connections.js +313 -19
- package/dist/connections.js.map +1 -1
- package/dist/middleware.js +47 -26
- package/dist/middleware.js.map +1 -1
- package/dist/server.d.ts +20 -2
- package/dist/server.js +65 -18
- package/dist/server.js.map +1 -1
- package/dist/typegen.d.ts +42 -10
- package/dist/typegen.js +116 -15
- package/dist/typegen.js.map +1 -1
- package/package.json +73 -68
package/README.md
CHANGED
|
@@ -1,210 +1,250 @@
|
|
|
1
|
-
# @geekapps/auth-nextjs
|
|
2
|
-
|
|
3
|
-
SDK Next.js para consumir o Geekapps Auth/IDP.
|
|
4
|
-
|
|
5
|
-
## Instalação
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun add @geekapps/auth-nextjs
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Server
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
// src/lib/auth.ts
|
|
15
|
-
import { configureGeekappsAuth } from "@geekapps/auth-nextjs/server";
|
|
16
|
-
|
|
17
|
-
export const auth = configureGeekappsAuth({
|
|
18
|
-
issuer: process.env.GEEKAPPS_AUTH_ISSUER!,
|
|
19
|
-
clientId: process.env.GEEKAPPS_AUTH_CLIENT_ID!,
|
|
20
|
-
clientSecret: process.env.GEEKAPPS_AUTH_CLIENT_SECRET,
|
|
21
|
-
redirectUri: process.env.GEEKAPPS_AUTH_REDIRECT_URI!,
|
|
22
|
-
scopes: ["openid", "profile", "email"],
|
|
23
|
-
});
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
// app/api/auth/[...routes]/route.ts
|
|
28
|
-
import { auth } from "@/lib/auth";
|
|
29
|
-
|
|
30
|
-
export const GET = auth.handlers.GET;
|
|
31
|
-
export const POST = auth.handlers.POST;
|
|
32
|
-
export const PATCH = auth.handlers.PATCH;
|
|
33
|
-
export const DELETE = auth.handlers.DELETE;
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Client
|
|
37
|
-
|
|
38
|
-
```tsx
|
|
39
|
-
import { GeekappsAuthProvider } from "@geekapps/auth-nextjs/client";
|
|
40
|
-
|
|
41
|
-
export function Providers({ children }: { children: React.ReactNode }) {
|
|
42
|
-
return <GeekappsAuthProvider>{children}</GeekappsAuthProvider>;
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Hooks principais:
|
|
47
|
-
|
|
48
|
-
- `useAuth()`
|
|
49
|
-
- `useOrg()` — organizações do próprio usuário (as que ele já é membro/dono)
|
|
50
|
-
- `useAccounts()`
|
|
51
|
-
- `useUsers()`
|
|
52
|
-
- `useRoles()`
|
|
53
|
-
- `usePermissions()`
|
|
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
|
|
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
|
-
```
|
|
102
|
-
|
|
103
|
-
## Organizações com CNPJ / Tax ID (`type: "legal"`)
|
|
104
|
-
|
|
105
|
-
Toda organização tem um `type`: `"standard"` (padrão, sem documento) ou `"legal"` (pessoa jurídica, associada a um Tax ID). O Tax ID é único por aplicação e não muda depois da criação.
|
|
106
|
-
|
|
107
|
-
- `taxIdType: "br_cnpj"` (padrão) — valida os dígitos verificadores (inclusive CNPJ alfanumérico) e **busca automaticamente** razão social, nome fantasia, contato e endereço na Receita (via BrasilAPI).
|
|
108
|
-
- `taxIdType: "generic"` — Tax ID de outro país; só é normalizado, e `taxIdCountry` (ISO, ex.: `"US"`) é obrigatório.
|
|
109
|
-
|
|
110
|
-
Obrigatórios de uma organização legal: `legalName` e `legalAddress.street`, `.city`, `.postalCode`, `.country`. O que a consulta não trouxer precisa ser informado pelo usuário.
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
// 1. Pré-preenche o formulário e descobre o que ainda falta pedir
|
|
114
|
-
const { lookupTaxId, createOrganization } = useOrg();
|
|
115
|
-
const result = await lookupTaxId("12.345.678/0001-95"); // type padrão: "br_cnpj"
|
|
116
|
-
if (!result.success) throw new Error(result.error);
|
|
117
|
-
const lookup = result.data;
|
|
118
|
-
// lookup.data → { legalName, tradeName, legalEmail, legalPhone, legalAddress }
|
|
119
|
-
// lookup.missingFields → ex.: ["legalAddress.postalCode"]
|
|
120
|
-
// lookup.alreadyRegistered → já existe org com esse CNPJ neste app
|
|
121
|
-
|
|
122
|
-
// 2. Cria — o servidor refaz a consulta e completa o que não foi enviado
|
|
123
|
-
const org = await createOrganization({
|
|
124
|
-
name: lookup.suggestedName ?? "Acme",
|
|
125
|
-
slug: "acme",
|
|
126
|
-
type: "legal",
|
|
127
|
-
taxId: lookup.taxId,
|
|
128
|
-
legalAddress: { postalCode: "01001000" }, // só o que faltou
|
|
129
|
-
});
|
|
130
|
-
// Se ainda faltar obrigatório: { success: false, code: "legal_fields_required", missingFields }.
|
|
131
|
-
|
|
132
|
-
// Tax ID genérico (outro país)
|
|
133
|
-
await createOrganization({
|
|
134
|
-
name: "Acme LLC", slug: "acme-llc", type: "legal",
|
|
135
|
-
taxId: "12-3456789", taxIdType: "generic", taxIdCountry: "US",
|
|
136
|
-
legalName: "Acme LLC",
|
|
137
|
-
legalAddress: { street: "1 Main St", city: "Austin", postalCode: "73301", country: "US" },
|
|
138
|
-
});
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Com `@geekapps/auth-elements-nextjs` (0.3+), o `CreateOrganizationForm` (também usado no `OrgSwitcher` e no `ManageOrganizationModal`) já entrega essa UI pronta: alterna Padrão/Empresa, busca o CNPJ e pede só os obrigatórios que faltarem.
|
|
142
|
-
|
|
143
|
-
## Papel do usuário na organização
|
|
144
|
-
|
|
145
|
-
`useOrg()` traz a participação do usuário logado na organização ativa, carregada sozinha ao trocar de organização:
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
const { currentMembership, getMyMembership, refreshMembership } = useOrg();
|
|
149
|
-
|
|
150
|
-
currentMembership?.role?.key; // ex.: "admin"
|
|
151
|
-
currentMembership?.isOwner; // dono passa em qualquer checagem
|
|
152
|
-
currentMembership?.permissions.includes("org:update");
|
|
153
|
-
|
|
154
|
-
const other = await getMyMembership(orgId); // outra organização
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Qualquer membro ativo pode listar os membros da organização com a role de cada um (`listMembers`). Editar, adicionar ou remover membros continua exigindo as permissões `org:members:*`.
|
|
158
|
-
|
|
159
|
-
## Convites recebidos pelo usuário
|
|
160
|
-
|
|
161
|
-
Para mostrar os convites dentro do app, sem depender do link do e-mail:
|
|
162
|
-
|
|
163
|
-
```tsx
|
|
164
|
-
const { listMyInvites, acceptInviteById, rejectInviteById } = useInvites();
|
|
165
|
-
|
|
166
|
-
const invites = await listMyInvites(); // pendentes para o e-mail da conta, só deste app
|
|
167
|
-
await acceptInviteById(invites[0].id); // cria a membership e atualiza as organizações
|
|
168
|
-
await rejectInviteById(invites[1].id);
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Só o próprio convidado (mesmo e-mail da conta) consegue aceitar ou recusar. Quem enviou o convite não recebe nenhum token.
|
|
172
|
-
|
|
173
|
-
## Standalone client auth actions
|
|
174
|
-
|
|
175
|
-
`@geekapps/auth-nextjs/client` exporta funções standalone usadas pela `auth-screens`:
|
|
176
|
-
|
|
177
|
-
```ts
|
|
178
|
-
import {
|
|
179
|
-
loginWithPassword,
|
|
180
|
-
registerWithPassword,
|
|
181
|
-
requestPasswordlessLink,
|
|
182
|
-
startGoogleLogin,
|
|
183
|
-
} from "@geekapps/auth-nextjs/client";
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Depois de aplicar esse source, rode o build do package para gerar o `dist`:
|
|
187
|
-
|
|
188
|
-
```bash
|
|
189
|
-
bun --filter @geekapps/auth-nextjs build
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
##
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
```
|
|
1
|
+
# @geekapps/auth-nextjs
|
|
2
|
+
|
|
3
|
+
SDK Next.js para consumir o Geekapps Auth/IDP.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @geekapps/auth-nextjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Server
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// src/lib/auth.ts
|
|
15
|
+
import { configureGeekappsAuth } from "@geekapps/auth-nextjs/server";
|
|
16
|
+
|
|
17
|
+
export const auth = configureGeekappsAuth({
|
|
18
|
+
issuer: process.env.GEEKAPPS_AUTH_ISSUER!,
|
|
19
|
+
clientId: process.env.GEEKAPPS_AUTH_CLIENT_ID!,
|
|
20
|
+
clientSecret: process.env.GEEKAPPS_AUTH_CLIENT_SECRET,
|
|
21
|
+
redirectUri: process.env.GEEKAPPS_AUTH_REDIRECT_URI!,
|
|
22
|
+
scopes: ["openid", "profile", "email"],
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// app/api/auth/[...routes]/route.ts
|
|
28
|
+
import { auth } from "@/lib/auth";
|
|
29
|
+
|
|
30
|
+
export const GET = auth.handlers.GET;
|
|
31
|
+
export const POST = auth.handlers.POST;
|
|
32
|
+
export const PATCH = auth.handlers.PATCH;
|
|
33
|
+
export const DELETE = auth.handlers.DELETE;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Client
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { GeekappsAuthProvider } from "@geekapps/auth-nextjs/client";
|
|
40
|
+
|
|
41
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
42
|
+
return <GeekappsAuthProvider>{children}</GeekappsAuthProvider>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Hooks principais:
|
|
47
|
+
|
|
48
|
+
- `useAuth()`
|
|
49
|
+
- `useOrg()` — organizações do próprio usuário (as que ele já é membro/dono)
|
|
50
|
+
- `useAccounts()`
|
|
51
|
+
- `useUsers()`
|
|
52
|
+
- `useRoles()`
|
|
53
|
+
- `usePermissions()`
|
|
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
|
|
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
|
+
```
|
|
102
|
+
|
|
103
|
+
## Organizações com CNPJ / Tax ID (`type: "legal"`)
|
|
104
|
+
|
|
105
|
+
Toda organização tem um `type`: `"standard"` (padrão, sem documento) ou `"legal"` (pessoa jurídica, associada a um Tax ID). O Tax ID é único por aplicação e não muda depois da criação.
|
|
106
|
+
|
|
107
|
+
- `taxIdType: "br_cnpj"` (padrão) — valida os dígitos verificadores (inclusive CNPJ alfanumérico) e **busca automaticamente** razão social, nome fantasia, contato e endereço na Receita (via BrasilAPI).
|
|
108
|
+
- `taxIdType: "generic"` — Tax ID de outro país; só é normalizado, e `taxIdCountry` (ISO, ex.: `"US"`) é obrigatório.
|
|
109
|
+
|
|
110
|
+
Obrigatórios de uma organização legal: `legalName` e `legalAddress.street`, `.city`, `.postalCode`, `.country`. O que a consulta não trouxer precisa ser informado pelo usuário.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// 1. Pré-preenche o formulário e descobre o que ainda falta pedir
|
|
114
|
+
const { lookupTaxId, createOrganization } = useOrg();
|
|
115
|
+
const result = await lookupTaxId("12.345.678/0001-95"); // type padrão: "br_cnpj"
|
|
116
|
+
if (!result.success) throw new Error(result.error);
|
|
117
|
+
const lookup = result.data;
|
|
118
|
+
// lookup.data → { legalName, tradeName, legalEmail, legalPhone, legalAddress }
|
|
119
|
+
// lookup.missingFields → ex.: ["legalAddress.postalCode"]
|
|
120
|
+
// lookup.alreadyRegistered → já existe org com esse CNPJ neste app
|
|
121
|
+
|
|
122
|
+
// 2. Cria — o servidor refaz a consulta e completa o que não foi enviado
|
|
123
|
+
const org = await createOrganization({
|
|
124
|
+
name: lookup.suggestedName ?? "Acme",
|
|
125
|
+
slug: "acme",
|
|
126
|
+
type: "legal",
|
|
127
|
+
taxId: lookup.taxId,
|
|
128
|
+
legalAddress: { postalCode: "01001000" }, // só o que faltou
|
|
129
|
+
});
|
|
130
|
+
// Se ainda faltar obrigatório: { success: false, code: "legal_fields_required", missingFields }.
|
|
131
|
+
|
|
132
|
+
// Tax ID genérico (outro país)
|
|
133
|
+
await createOrganization({
|
|
134
|
+
name: "Acme LLC", slug: "acme-llc", type: "legal",
|
|
135
|
+
taxId: "12-3456789", taxIdType: "generic", taxIdCountry: "US",
|
|
136
|
+
legalName: "Acme LLC",
|
|
137
|
+
legalAddress: { street: "1 Main St", city: "Austin", postalCode: "73301", country: "US" },
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Com `@geekapps/auth-elements-nextjs` (0.3+), o `CreateOrganizationForm` (também usado no `OrgSwitcher` e no `ManageOrganizationModal`) já entrega essa UI pronta: alterna Padrão/Empresa, busca o CNPJ e pede só os obrigatórios que faltarem.
|
|
142
|
+
|
|
143
|
+
## Papel do usuário na organização
|
|
144
|
+
|
|
145
|
+
`useOrg()` traz a participação do usuário logado na organização ativa, carregada sozinha ao trocar de organização:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const { currentMembership, getMyMembership, refreshMembership } = useOrg();
|
|
149
|
+
|
|
150
|
+
currentMembership?.role?.key; // ex.: "admin"
|
|
151
|
+
currentMembership?.isOwner; // dono passa em qualquer checagem
|
|
152
|
+
currentMembership?.permissions.includes("org:update");
|
|
153
|
+
|
|
154
|
+
const other = await getMyMembership(orgId); // outra organização
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Qualquer membro ativo pode listar os membros da organização com a role de cada um (`listMembers`). Editar, adicionar ou remover membros continua exigindo as permissões `org:members:*`.
|
|
158
|
+
|
|
159
|
+
## Convites recebidos pelo usuário
|
|
160
|
+
|
|
161
|
+
Para mostrar os convites dentro do app, sem depender do link do e-mail:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
const { listMyInvites, acceptInviteById, rejectInviteById } = useInvites();
|
|
165
|
+
|
|
166
|
+
const invites = await listMyInvites(); // pendentes para o e-mail da conta, só deste app
|
|
167
|
+
await acceptInviteById(invites[0].id); // cria a membership e atualiza as organizações
|
|
168
|
+
await rejectInviteById(invites[1].id);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Só o próprio convidado (mesmo e-mail da conta) consegue aceitar ou recusar. Quem enviou o convite não recebe nenhum token.
|
|
172
|
+
|
|
173
|
+
## Standalone client auth actions
|
|
174
|
+
|
|
175
|
+
`@geekapps/auth-nextjs/client` exporta funções standalone usadas pela `auth-screens`:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import {
|
|
179
|
+
loginWithPassword,
|
|
180
|
+
registerWithPassword,
|
|
181
|
+
requestPasswordlessLink,
|
|
182
|
+
startGoogleLogin,
|
|
183
|
+
} from "@geekapps/auth-nextjs/client";
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Depois de aplicar esse source, rode o build do package para gerar o `dist`:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
bun --filter @geekapps/auth-nextjs build
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## App Connections: conectar e chamar outra app
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
"use server";
|
|
196
|
+
import { connectApp } from "@geekapps/auth-nextjs/connections";
|
|
197
|
+
|
|
198
|
+
export async function connectPost() {
|
|
199
|
+
await connectApp({
|
|
200
|
+
app: "post", // slug do catálogo, com autocomplete pelos tipos gerados
|
|
201
|
+
scopes: ["messages:send"], // tipados pela app; o usuário vê a descrição de cada um no consent
|
|
202
|
+
redirectUri: "https://minha-app.com/api/connections/callback",
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Alternativa sem slug: connectApp({ targetAppId: "<client_id da app alvo>", scopes: [...], redirectUri })
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
No callback, `completeAppConnection({ code, state })` devolve o API token da conexão (guarde-o cifrado). Para usar:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { AppConnectionError, connections } from "@geekapps/auth-nextjs/connections";
|
|
212
|
+
|
|
213
|
+
const post = connections.post(connectionToken);
|
|
214
|
+
// Só as rotas dos escopos pedidos, nos tipos:
|
|
215
|
+
// const post = connections.post(connectionToken, { scopes: ["messages:send"] });
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const message = await post.messages.send({ channel, to, text });
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof AppConnectionError && error.code === "missing_scope") {
|
|
221
|
+
// peça a conexão de novo com o escopo (error.scope)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- Params, query e body vão num objeto só; o SDK separa pelo manifesto da app alvo (params no path, query em GET/DELETE, o resto no body).
|
|
227
|
+
- O retorno é tipado pela resposta 200/201 da rota. O JWT da conexão é trocado, guardado em cache e renovado sozinho.
|
|
228
|
+
- `AppConnectionError.code`: `"missing_scope" | "connection_revoked" | "connection_expired" | "validation_error" | "network_error" | "unknown"` (o código cru do servidor fica em `reason`).
|
|
229
|
+
- Com outra configuração que não o `.env`: `createConnections({ issuer, clientId, clientSecret })`.
|
|
230
|
+
- `createConnectionClient` (OpenAPI + `openapi-fetch`) e `connectionFetch` continuam funcionando, mas estão `@deprecated`.
|
|
231
|
+
|
|
232
|
+
## Tipos das App Connections
|
|
233
|
+
|
|
234
|
+
Em desenvolvimento (`NODE_ENV=development`), o SDK lê o manifesto (`/.well-known/geekapps-app.json`) de cada app conectável e escreve os tipos em `geekapps-connections/` (escopos, rotas e entrada/saída de cada rota), ao subir e depois a cada minuto. Só é preciso ter o `json-schema-to-typescript` como devDependency (e `openapi-typescript`, se ainda usar `createConnectionClient`):
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npm i -D json-schema-to-typescript
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Sem ele, a geração simplesmente não roda e o app funciona normalmente. Para desligar, use `GEEKAPPS_CONNECTIONS_TYPES=off`. Para checar no CI se os tipos estão em dia:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { generateConnectionTypes } from "@geekapps/auth-nextjs/typegen";
|
|
244
|
+
|
|
245
|
+
const { stale } = await generateConnectionTypes({
|
|
246
|
+
credentials: { issuer, clientId, clientSecret },
|
|
247
|
+
check: true,
|
|
248
|
+
});
|
|
249
|
+
if (stale.length) process.exit(1);
|
|
250
|
+
```
|
package/dist/actions.js
CHANGED
|
@@ -25,15 +25,6 @@ function resolveIssuer() {
|
|
|
25
25
|
env("GEEKAPPS_AUTH_ISSUER") ?? env("GEEKAPPS_AUTH_API_URL") ?? GEEKAPPS_AUTH_SERVER_URL
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
|
-
function appendClientCredentials(url) {
|
|
29
|
-
const clientId = env("GEEKAPPS_AUTH_CLIENT_ID");
|
|
30
|
-
const clientSecret = env("GEEKAPPS_AUTH_CLIENT_SECRET");
|
|
31
|
-
if (!clientId || !clientSecret) return url;
|
|
32
|
-
const u = new URL(url);
|
|
33
|
-
u.searchParams.set("client_id", clientId);
|
|
34
|
-
u.searchParams.set("client_secret", clientSecret);
|
|
35
|
-
return u.toString();
|
|
36
|
-
}
|
|
37
28
|
function hashClientId(clientId) {
|
|
38
29
|
let h = 5381;
|
|
39
30
|
for (let i = 0; i < clientId.length; i++) {
|
|
@@ -141,7 +132,7 @@ async function authFetch(path) {
|
|
|
141
132
|
let accessToken = await getActiveAccessToken();
|
|
142
133
|
if (!accessToken) throw new Error("Not authenticated");
|
|
143
134
|
const issuer = resolveIssuer();
|
|
144
|
-
const doFetch = (token) => fetch(
|
|
135
|
+
const doFetch = (token) => fetch(`${issuer}${path}`, {
|
|
145
136
|
headers: { authorization: `Bearer ${token}` },
|
|
146
137
|
cache: "no-store"
|
|
147
138
|
});
|