feat(auth): implement user and device authentication

This commit is contained in:
QiuSW
2026-07-26 15:18:48 +08:00
parent c5d3b215ff
commit 49db5b8305
66 changed files with 6216 additions and 271 deletions
+67 -2
View File
@@ -15,10 +15,14 @@ import (
"cmroubao/backend-api/internal/platform/assetstore"
"cmroubao/backend-api/internal/platform/database"
"cmroubao/backend-api/internal/platform/migration"
"cmroubao/backend-api/internal/platform/password"
repository "cmroubao/backend-api/internal/repository/sqlite"
"cmroubao/backend-api/internal/transport/authcommon"
"cmroubao/backend-api/internal/transport/httpapi"
"cmroubao/backend-api/internal/transport/webui"
"cmroubao/backend-api/internal/usecase"
"github.com/gin-gonic/gin"
)
func main() {
@@ -67,6 +71,13 @@ func run() error {
serverErrors := make(chan error, 1)
go func() {
if cfg.TLSCertificate != "" {
serverErrors <- server.ListenAndServeTLS(
cfg.TLSCertificate,
cfg.TLSPrivateKey,
)
return
}
serverErrors <- server.ListenAndServe()
}()
@@ -158,6 +169,20 @@ func buildRouter(
if err != nil {
return nil, err
}
passwords, err := password.NewBcrypt(12)
if err != nil {
return nil, err
}
auth, err := usecase.NewAuthService(
store,
passwords,
clock,
ids,
usecase.CryptoTokenGenerator{},
)
if err != nil {
return nil, err
}
webService, err := webui.NewUsecaseAdapter(tasks, assets)
if err != nil {
@@ -171,6 +196,33 @@ func buildRouter(
if err != nil {
return nil, err
}
authWebService, err := webui.NewAuthUsecaseAdapter(auth)
if err != nil {
return nil, err
}
loginLimiter, err := authcommon.NewAttemptLimiter(
10,
5*time.Minute,
4096,
)
if err != nil {
return nil, err
}
authWebHandler, err := webui.NewAuthHandler(
authWebService,
renderer,
loginLimiter,
)
if err != nil {
return nil, err
}
registerPublicAuth, err := httpapi.NewPublicAuthRegistrar(
auth,
loginLimiter,
)
if err != nil {
return nil, err
}
registerAdminRoutes, err := httpapi.NewAdminRouteRegistrar(
httpapi.AdminServices{
Assets: assets,
@@ -181,9 +233,22 @@ func buildRouter(
if err != nil {
return nil, err
}
registerProtectedRoutes := func(routes gin.IRoutes) error {
if err := registerAdminRoutes(routes); err != nil {
return err
}
authWebHandler.RegisterProtected(routes)
return nil
}
return httpapi.NewRouter(httpapi.RouterDependencies{
Database: db,
RegisterAdminRoutes: registerAdminRoutes,
Database: db,
RegisterPublicRoutes: func(routes gin.IRoutes) error {
webHandler.RegisterStatic(routes)
authWebHandler.RegisterPublic(routes)
return registerPublicAuth(routes)
},
RegisterAdminRoutes: registerProtectedRoutes,
AdminSessions: auth,
LogEvent: func(event string) {
log.Print(event)
},
+43
View File
@@ -5,9 +5,13 @@ import (
"errors"
"net"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"time"
"cmroubao/backend-api/internal/config"
"cmroubao/backend-api/internal/platform/database"
"cmroubao/backend-api/internal/platform/migration"
)
@@ -114,6 +118,45 @@ func TestRequireCurrentMigrationsHidesStatusFailure(t *testing.T) {
}
}
func TestBuildRouterRegistersProtectedLogoutRoute(t *testing.T) {
ctx := context.Background()
db, err := database.Open(
ctx,
filepath.Join(t.TempDir(), "api.db"),
)
if err != nil {
t.Fatalf("database.Open() error = %v", err)
}
defer db.Close()
runner, err := migration.New(db)
if err != nil {
t.Fatalf("migration.New() error = %v", err)
}
if _, err := runner.Up(ctx); err != nil {
t.Fatalf("migration.Up() error = %v", err)
}
router, err := buildRouter(ctx, config.Config{
AssetDirectory: filepath.Join(t.TempDir(), "assets"),
}, db)
if err != nil {
t.Fatalf("buildRouter() error = %v", err)
}
request := httptest.NewRequest(http.MethodPost, "/logout", nil)
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusSeeOther ||
response.Header().Get("Location") !=
"/login?next=%2Ftasks" {
t.Fatalf(
"status/location = %d / %q",
response.Code,
response.Header().Get("Location"),
)
}
}
type stubMigrationStatusReader struct {
statuses []migration.Status
err error