1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package rpc
4
5import (
6 "context"
7 "encoding/base64"
8 "errors"
9 "fmt"
10
11 "connectrpc.com/connect"
12
13 pb "bnema.dev/ermite-api/gen/ermite"
14 "bnema.dev/ermite/internal/domain"
15 "bnema.dev/ermite/internal/ports"
16)
17
18// errPasskeysUnavailable is what the passkey RPCs answer when the server runs
19// without passkeys (no https public URL with a DNS host name).
20var errPasskeysUnavailable = connect.NewError(connect.CodeFailedPrecondition, errors.New("passkeys are not available on this server"))
21
22// sessionService implements ermiteconnect.SessionServiceHandler. The browser
23// never calls it: sessions come from the link or a passkey, and the API
24// ignores cookies. passkeys is nil when passkeys are not available.
25type sessionService struct {
26 sessions ports.SessionService
27 passkeys ports.PasskeyService
28}
29
30func (s *sessionService) CreateLoginLink(ctx context.Context, _ *connect.Request[pb.CreateLoginLinkRequest]) (*connect.Response[pb.CreateLoginLinkResponse], error) {
31 link, err := s.sessions.CreateLoginLink(ctx, caller(ctx))
32 if err != nil {
33 return nil, connectError(ctx, err)
34 }
35 return connect.NewResponse(&pb.CreateLoginLinkResponse{Url: link.URL, ExpiresAt: timestamp(link.ExpiresAt)}), nil
36}
37
38func (s *sessionService) SignOutEverywhere(ctx context.Context, _ *connect.Request[pb.SignOutEverywhereRequest]) (*connect.Response[pb.SignOutEverywhereResponse], error) {
39 if err := s.sessions.SignOutEverywhere(ctx, caller(ctx)); err != nil {
40 return nil, connectError(ctx, err)
41 }
42 return connect.NewResponse(&pb.SignOutEverywhereResponse{}), nil
43}
44
45func (s *sessionService) ListPasskeys(ctx context.Context, _ *connect.Request[pb.ListPasskeysRequest]) (*connect.Response[pb.ListPasskeysResponse], error) {
46 if s.passkeys == nil {
47 return nil, errPasskeysUnavailable
48 }
49 list, err := s.passkeys.List(ctx, caller(ctx))
50 if err != nil {
51 return nil, connectError(ctx, err)
52 }
53 return connect.NewResponse(&pb.ListPasskeysResponse{Passkeys: passkeysToPB(list)}), nil
54}
55
56func (s *sessionService) RemovePasskey(ctx context.Context, req *connect.Request[pb.RemovePasskeyRequest]) (*connect.Response[pb.RemovePasskeyResponse], error) {
57 if s.passkeys == nil {
58 return nil, errPasskeysUnavailable
59 }
60 id, err := base64.RawURLEncoding.DecodeString(req.Msg.GetId())
61 if err != nil || len(id) == 0 {
62 return nil, connectError(ctx, fmt.Errorf("%w: id is not a base64url credential id", domain.ErrInvalid))
63 }
64 if err := s.passkeys.Remove(ctx, caller(ctx), id); err != nil {
65 return nil, connectError(ctx, err)
66 }
67 return connect.NewResponse(&pb.RemovePasskeyResponse{}), nil
68}