authkit initial

This commit is contained in:
juancwu 2026-04-26 01:36:53 +00:00
commit 134393fbca
43 changed files with 5188 additions and 1 deletions

62
service_magic.go Normal file
View file

@ -0,0 +1,62 @@
package authkit
import (
"context"
"git.juancwu.dev/juancwu/errx"
)
// RequestMagicLink mints a single-use magic-link token for the email and
// returns the plaintext for delivery. ErrUserNotFound is returned for
// unregistered emails.
func (a *Auth) RequestMagicLink(ctx context.Context, email string) (string, error) {
const op = "authkit.Auth.RequestMagicLink"
u, err := a.deps.Users.GetUserByEmail(ctx, normalizeEmail(email))
if err != nil {
return "", errx.Wrap(op, err)
}
plaintext, hash, err := mintSecret(prefixMagicLink, a.cfg.Random)
if err != nil {
return "", errx.Wrap(op, err)
}
now := a.now()
t := &Token{
Hash: hash,
Kind: TokenMagicLink,
UserID: u.ID,
CreatedAt: now,
ExpiresAt: now.Add(a.cfg.MagicLinkTTL),
}
if err := a.deps.Tokens.CreateToken(ctx, t); err != nil {
return "", errx.Wrap(op, err)
}
return plaintext, nil
}
// ConsumeMagicLink consumes the magic-link token and returns the
// authenticated user. Callers typically follow this with IssueSession or
// IssueJWT to actually log the user in.
func (a *Auth) ConsumeMagicLink(ctx context.Context, plaintextToken string) (*User, error) {
const op = "authkit.Auth.ConsumeMagicLink"
hash, ok := parseSecret(prefixMagicLink, plaintextToken)
if !ok {
return nil, errx.Wrap(op, ErrTokenInvalid)
}
now := a.now()
t, err := a.deps.Tokens.ConsumeToken(ctx, TokenMagicLink, hash, now)
if err != nil {
return nil, errx.Wrap(op, err)
}
u, err := a.deps.Users.GetUserByID(ctx, t.UserID)
if err != nil {
return nil, errx.Wrap(op, err)
}
// A successful magic-link login also implicitly verifies the email
// (the user demonstrably controls the inbox).
if u.EmailVerifiedAt == nil {
if err := a.deps.Users.SetEmailVerified(ctx, u.ID, now); err == nil {
u.EmailVerifiedAt = &now
}
}
return u, nil
}