117 lines
4.2 KiB
Go
117 lines
4.2 KiB
Go
// Package userservice is the SciMesh authentication service, embedded into the
|
|
// coordinator binary for single-binary deployments. The packages here are the
|
|
// same code the standalone `users/` service runs, with its PostgreSQL storage
|
|
// replaced by an embedded SQLite backend. The coordinator's HTTP layer talks
|
|
// to it through the usual USERSERVICE_URL proxy, so no proxy code changes.
|
|
package userservice
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/userservice/auth"
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/userservice/storage/sqlite"
|
|
usershttp "github.com/emil28092005/SciMesh/coordinator/internal/userservice/transport/http"
|
|
"github.com/emil28092005/SciMesh/coordinator/internal/userservice/usecase"
|
|
)
|
|
|
|
// Config wires the embedded userservice.
|
|
type Config struct {
|
|
// DBPath is the sqlite database file (for example <data-dir>/users.db).
|
|
DBPath string
|
|
// JWTSecret must equal the coordinator's JWT_SECRET so tokens verify.
|
|
JWTSecret string
|
|
// AdminEmail/AdminPassword bootstrap the first admin on first run.
|
|
AdminEmail string
|
|
AdminPassword string
|
|
// Log receives the service's log lines.
|
|
Log *slog.Logger
|
|
}
|
|
|
|
// Serve runs the embedded userservice until ctx is cancelled. It listens only
|
|
// on the loopback interface; the coordinator proxies to it internally.
|
|
func Serve(ctx context.Context, cfg Config) (string, func() error, error) {
|
|
db, err := sqlite.Open(cfg.DBPath)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if err := sqlite.Migrate(ctx, db, cfg.Log); err != nil {
|
|
_ = db.Close()
|
|
return "", nil, err
|
|
}
|
|
|
|
clock := NewClock()
|
|
users := sqlite.NewUserRepo(db)
|
|
workerKeys := sqlite.NewWorkerKeyRepo(db)
|
|
hasher := auth.NewHasher(0)
|
|
issuer := auth.NewIssuer(cfg.JWTSecret, 24*time.Hour, clock.Now)
|
|
|
|
uc := usershttp.UseCases{
|
|
Register: usecase.NewRegister(users, hasher, clock),
|
|
Login: usecase.NewLogin(users, hasher, issuer),
|
|
SetVerified: usecase.NewSetVerified(users),
|
|
SetRole: usecase.NewSetRole(users),
|
|
CreateWorkerKey: usecase.NewCreateWorkerKey(workerKeys, clock),
|
|
ListWorkerKeys: usecase.NewListWorkerKeys(workerKeys),
|
|
ListWorkerKeysAll: usecase.NewListWorkerKeysAll(workerKeys),
|
|
RevokeWorkerKey: usecase.NewRevokeWorkerKey(workerKeys),
|
|
RevokeWorkerKeyAdmin: usecase.NewRevokeWorkerKeyAdmin(workerKeys),
|
|
ExchangeWorkerKey: usecase.NewExchangeWorkerKey(workerKeys, users, issuer, 24*time.Hour),
|
|
ListUsers: usecase.NewListUsers(users),
|
|
Users: users,
|
|
}
|
|
|
|
if cfg.AdminEmail != "" && cfg.AdminPassword != "" {
|
|
created, err := usecase.NewBootstrapAdmin(users, hasher, clock).
|
|
Execute(ctx, cfg.AdminEmail, cfg.AdminPassword)
|
|
if err != nil {
|
|
_ = db.Close()
|
|
return "", nil, fmt.Errorf("bootstrap admin: %w", err)
|
|
}
|
|
if created {
|
|
cfg.Log.Info("embedded userservice created the admin account", "email", cfg.AdminEmail)
|
|
}
|
|
}
|
|
|
|
handler := usershttp.NewServer(cfg.Log, uc, issuer)
|
|
handler = http.TimeoutHandler(handler, 15*time.Second, `{"error":"request timeout"}`)
|
|
|
|
// Bind an ephemeral loopback port so a second serve instance can never
|
|
// collide with the first; the coordinator's proxy uses the returned
|
|
// address and needs no fixed-port assumption.
|
|
listener, err := (&net.ListenConfig{}).Listen(ctx, "tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
_ = db.Close()
|
|
return "", nil, fmt.Errorf("listen for embedded userservice: %w", err)
|
|
}
|
|
server := &http.Server{
|
|
Handler: handler,
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
go func() {
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
|
defer cancel()
|
|
_ = server.Shutdown(shutdownCtx)
|
|
}()
|
|
go func() {
|
|
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
|
|
cfg.Log.Error("embedded userservice stopped", "err", err)
|
|
}
|
|
}()
|
|
return listener.Addr().String(), func() error { return db.Close() }, nil
|
|
}
|
|
|
|
// NewClock returns the userservice's wall clock.
|
|
func NewClock() *Clock { return &Clock{} }
|
|
|
|
// Clock implements the userservice usecase clock.
|
|
type Clock struct{}
|
|
|
|
// Now returns the current UTC time.
|
|
func (c *Clock) Now() time.Time { return time.Now().UTC() }
|