# Receita: Um app na frente de uma API existente > Framework web para Go no estilo Next.js: uma pasta em app/ é uma rota, o HTML é escrito em Go e nada fora da biblioteca padrão é importado. > /trilha/pt/llms/recipes/api-existente.txt - [Um app na frente de uma API existente](/trilha/pt/receitas/api-existente): Encaminhar /api/ para um serviço que já existe, com um login próprio. · 3132 tokens est. - [Upstreams](/trilha/pt/referencia/upstreams): Encaminhar um prefixo de URL para uma API que já existe, com a credencial da sessão. · 1258 tokens est. - [auth](/trilha/pt/referencia/auth): Provider, Options, Auth, User e Store — a API do pacote auth, com os padrões e o que cada campo muda. · 10704 tokens est. 3 page(s) · 15094 tokens est. in all --- A forma da maioria das migrações: os usuários são seus — e-mail, hash de senha, papel — e a API continua onde está. Duas peças do Trilha se encontram aqui, e se encontram porque o proxy precisa levar o que o login guardou. O `examples/local-login` é esta receita, inteira e rodando. ## 1. A sessão, sem provedor `auth.Sessions` é o mesmo `*Auth` do fluxo OIDC, sem provedor. `Require()`, `RequireRole()`, a `Store`, a rotação do identificador e a janela idle são o código que já estava lá; o que muda é quem diz que a pessoa é quem diz ser. ```go var Flow = auth.Sessions(auth.Options{ Store: auth.NewMemoryStore(), Idle: 30 * time.Minute, LoginPath: "/entrar", AfterLogin: "/painel", }) ``` `User.Extra` é o que esta sessão carrega e para o que o OIDC não tem claim — o token que a API espera, o tenant, o plano. Ele viaja por onde o resto da sessão viaja, então deixe-o pequeno e nunca ponha uma senha ali: ```go func Entrar(c *trilha.Ctx, u usuarios.Usuario) error { return Flow.Login(c, &auth.User{ Subject: u.ID, Email: u.Email, Name: u.Nome, Roles: []string{u.Papel}, // The token of the API the upstream forwards to. It travels in the // session, never in the page. Extra: map[string]string{"api_token": u.Token}, }) } ``` ## 2. A senha, conferida na sua própria tabela `auth.CheckPBKDF2` lê o formato que o Django e o `hashlib` gravam — `pbkdf2_sha256$iteracoes$sal$hash` — então a tabela de usuários que já existe continua valendo, hashes e tudo. `auth.HashPBKDF2` grava um novo com 600 mil iterações. Responda a mesma coisa para e-mail errado e para senha errada, e demore o mesmo tanto: dizer qual dos dois estava errado conta a um atacante quem tem conta. ```go func (s *Store) Verify(email, senha string) (Usuario, error) { s.mu.RLock() u, ok := s.rows[strings.ToLower(strings.TrimSpace(email))] s.mu.RUnlock() if !ok { auth.CheckPBKDF2(semUsuario, senha) return Usuario{}, ErrCredencial } if !auth.CheckPBKDF2(u.Hash, senha) { return Usuario{}, ErrCredencial } return u, nil } ``` O handler é um formulário como outro qualquer: ```go func POST(c *trilha.Ctx) error { email, senha := c.Form("email"), c.Form("senha") u, err := trilha.Use[*usuarios.Store](c).Verify(email, senha) if err != nil { return c.Render(422, formulario(c, email, "E-mail ou senha inválidos.")) } return sessao.Entrar(c, u) } ``` ## 3. A API, na mesma origem ```go func Config(cfg *trilha.Config) { // Where the trail of who did what goes. Without this it would go to the // log, which is enough to grep and enough to ship with. cfg.Audit = sessao.Auditoria // O app é escrito para quem lê em português, e é isto que decide o idioma // dos componentes do kit que trazem texto próprio — a tela de auditoria // entre eles — e o fuso em que as datas dela aparecem. cfg.Locale = "pt-BR" cfg.TimeZone = "America/Sao_Paulo" if api := os.Getenv("API_URL"); api != "" { cfg.Upstreams = map[string]trilha.Upstream{ "/api/": { Target: api, Headers: func(c *trilha.Ctx, hdr http.Header) { if tok := sessao.Token(c); tok != "" { hdr.Set("Authorization", "Bearer "+tok) } }, }, } } } ``` O proxy responde fora da cadeia de middleware, então ninguém pôs o usuário no contexto da requisição: leia a sessão do cookie. ```go func Token(c *trilha.Ctx) string { u, err := Flow.Session(c) if err != nil { return "" } return u.Extra["api_token"] } ``` ## Os tipos que a API já publica O proxy acima é para o browser. A página não é o browser: ela roda no servidor, e o que ela precisa não é de uma URL, é da forma da resposta — que a API já publica, no documento OpenAPI dela. ```bash trilha client http://api:8801/openapi.json --out internal/api ``` Isso grava um arquivo só, commitado como o `trilha_gen.go`, com uma struct por esquema e um método por operação. O `trilha client ... --check` na CI falha no dia em que o documento mudar e ninguém tiver regerado. O cliente gerado importa a biblioteca padrão e mais nada — nem a Trilha — então serve também num job e num teste. ```go // Acervo is the client of the API the front used to call from the browser. It // is generated by `trilha client` from the document the API publishes, so the // compiler is what catches the field that changed name. var Acervo = api.New("http://api:8801", api.WithHeader(sessionToken), api.WithResponse(collectCookies)) ``` A credencial viaja no contexto, não no cliente: ```go // tokenKey is how the credential travels from the request to the client. The // client takes a context, not a *trilha.Ctx: it also runs in a job and in a // test, where there is no request to borrow a token from. type tokenKey struct{} // Calling is the context to hand the client: the session's token, and nothing // else about the request. func Calling(c *trilha.Ctx) context.Context { tok, _ := c.Signed("api_token") return context.WithValue(c.Context(), tokenKey{}, tok) } // sessionToken is the only place the credential appears. It runs for every // request the client makes, so a call from a context that carries no token // goes out unauthenticated instead of borrowing somebody else's session. func sessionToken(ctx context.Context) http.Header { tok, _ := ctx.Value(tokenKey{}).(string) if tok == "" { return nil } return http.Header{"Authorization": {"Bearer " + tok}} } ``` Quando a API é autenticada por **cookie**, a credencial vai no sentido contrário: ela volta num `Set-Cookie`, e a operação que a produz é o login. O `WithResponse` roda para cada resposta que o cliente recebe e entrega o status e os cabeçalhos dela, então o coletor é a mesma ideia do token — mora no context da chamada, e é desta requisição: ```go // cookieKey is the other direction of tokenKey. On an API authenticated by // cookie the credential comes back in a Set-Cookie, and the operation that // produces it is the login — so the page has to read the response and not only // the body. The collector travels in the context of the call, which is what makes // it right on a server: a http.CookieJar on the client would belong to the // process, and would send one person's credential on the next person's call. type cookieKey struct{} // Collecting is Calling for an operation whose answer is also in the header: the // context to hand the client, and where the cookies it set will be once the call // returns. func Collecting(c *trilha.Ctx) (context.Context, *[]*http.Cookie) { var set []*http.Cookie return context.WithValue(Calling(c), cookieKey{}, &set), &set } // collectCookies runs for every response the client receives, and fills the // collector of that call — nothing at all when the call did not bring one. func collectCookies(ctx context.Context, resp *http.Response) { if set, ok := ctx.Value(cookieKey{}).(*[]*http.Cookie); ok { *set = resp.Cookies() } } // SignIn hands the browser the credential the API set. The operation is whichever // one the API calls a login — here, the one this document happens to declare — // and what matters is that its Set-Cookie reaches the response of this request // and of no other. func SignIn(c *trilha.Ctx, in api.User) error { ctx, cookies := Collecting(c) if _, err := Acervo.Users().UpsertUser(ctx, in); err != nil { return err } for _, ck := range *cookies { ck.Domain = "" // the cookie is this app's host now, not the API's c.SetCookie(ck) } return c.Redirect("/documents") } ``` O mesmo gancho lê o `ETag` ou o `Location` de um `POST` que cria e o `Link` de uma resposta paginada. O que ele não oferece é o corpo: o corpo é da operação, que o devolve decodificado. E aí a página é uma página. Não há estado de carregamento, porque o dado já está na mão na hora de escrever o HTML: ```go // Documents is the page. It runs on the server, so there is no loading state // and no second endpoint: the data it renders is already in hand. func Documents(c *trilha.Ctx) (h.Node, error) { var q DocsQuery if err := c.Bind(&q); err != nil { return nil, err } page, err := Acervo.Documents().ListDocuments(Calling(c), api.DocumentsListDocumentsParams{ Page: int64(q.Page), PageSize: int64(q.PerPage), Q: q.Q, Status: api.Status(q.Status), }) if err != nil { return nil, err } q.ListParams.Restrict("filename", "status") return ui.DataTable(c, DocsColumns, page.Items, ui.ListState{ Params: q.ListParams, Total: int(page.Total), ID: "docs", Search: "search", }), nil } ``` As colunas são tipadas pela struct da própria API, então um campo que deixa de existir deixa de compilar — que é o motivo de gerar o cliente em vez de ler JSON para dentro de um mapa: ```go // DocsColumns names the columns of the table. The types are the API's, so a // field that stops existing stops compiling. var DocsColumns = ui.Columns[api.Document]{ {Key: "filename", Label: "File", Sort: true}, {Key: "status", Label: "Status", Cell: func(d api.Document) h.Node { return ui.Badge(h.Text(string(d.Status))) }}, {Key: "pages", Label: "Pages", Num: true, Cell: func(d api.Document) h.Node { return h.Text(strconv.Itoa(int(d.Pages))) }}, } ``` Resposta binária chega como o `*http.Response`, então o corpo passa direto: ```go // DownloadDocument hands the API's answer straight to the browser: the body is // never read into memory, and the Content-Type the API chose is the one that // arrives. func DownloadDocument(c *trilha.Ctx) error { resp, err := Acervo.Documents().Download(Calling(c), c.Param("id")) if err != nil { return err } defer resp.Body.Close() return c.Inline(c.Param("id")+".pdf", resp.Body, resp.Header.Get("Content-Type")) } ``` As duas portas são a mesma API e o mesmo token: a página usa o cliente, as ilhas e qualquer `fetch` continuam chamando `/api/*` nesta origem. ```go // ProxyAPI is the other half. The page uses the client above; the islands and // any fetch still call /api/* on the same origin, and the upstream puts the // same credential on the request. One API, one token, two doors. func ProxyAPI(cfg *trilha.Config) { cfg.Upstreams = map[string]trilha.Upstream{ "/api/": { Target: "http://api:8801", Headers: func(c *trilha.Ctx, hdr http.Header) { if tok, _ := c.Signed("api_token"); tok != "" { hdr.Set("Authorization", "Bearer "+tok) } }, Timeout: 60 * time.Second, }, } } ``` Uma escrita recusada volta como um `*Error`, e quando a API é uma FastAPI o 422 que ela escreve já é uma mensagem por campo. `Fields` é essa lista, com a chave que o formulário usa, então a página que falhou se redesenha com cada mensagem ao lado do seu campo: ```go // CreateDocument is the write. A 422 is not an error page: it is the same form // again, with the API's own messages next to the fields that earned them. func CreateDocument(c *trilha.Ctx) (h.Node, error) { var in api.DocumentIn if err := c.Bind(&in); err != nil { return nil, err } doc, err := Acervo.Documents().CreateDocument(Calling(c), in) if e, ok := api.AsError(err); ok && e.Status == http.StatusUnprocessableEntity { return documentForm(in, trilha.FieldErrors(e.Fields)), nil } if err != nil { return nil, err } return nil, c.Redirect("/documents/" + doc.ID) } ``` `trilha.FieldErrors` é um `map[string]string` e `Fields` também, então a conversão é o adaptador inteiro: nenhuma segunda validação das mesmas regras deste lado, e nenhuma versão delas para divergir da API. O que o gerador não vai fingir que entende — um `oneOf`, um esquema sem tipo — chega como `json.RawMessage` e sai como uma linha do relatório na hora de gerar; uma API sem `response_model` em lugar nenhum ganha a conta de quanto dela ficou sem tipo. Veja [`trilha client`](/trilha/pt/referencia/cli#trilha-client). ## O que vem de graça - `POST /api/*` exige o token do CSRF, como toda escrita do app. - O `Authorization` do próprio navegador nunca chega à API — só o da sessão. - Um upload de 200 MB e um PDF voltando passam em stream, por cima do `MaxBodyBytes` e do write deadline. - Um `route.go` que você escreva em `/api/documentos` responde antes do proxy, então a API vai para Go endpoint a endpoint e o front não percebe. O `trilha audit` avisa se o alvo é `http://` puro para outro host, se o upstream não injeta nada num app com login, ou se o login está sem limite de taxa. Veja [Upstreams](/trilha/pt/referencia/upstreams) e [Auth](/trilha/pt/referencia/auth) para os detalhes.