package main import ( "context" "errors" "fmt" "io" "log" "os" "strings" "time" "cmroubao/backend-api/internal/config" "cmroubao/backend-api/internal/domain" "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/usecase" ) const passwordEnvironment = "CMROUBAO_AUTH_PASSWORD" func main() { if err := run( os.Args[1:], os.LookupEnv, os.Stdout, ); err != nil { log.Printf("auth command failed: %v", err) os.Exit(1) } } func run( arguments []string, lookup config.LookupEnvironment, output io.Writer, ) error { command, value, role, err := parseArguments(arguments) if err != nil { return err } databasePath, err := config.LoadDatabasePath(lookup) if err != nil { return err } var plainPassword string if command == "create-user" || command == "reset-password" { var exists bool plainPassword, exists = lookup(passwordEnvironment) if !exists || plainPassword == "" { return errors.New( passwordEnvironment + " must be set for " + command, ) } } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() db, err := database.Open(ctx, databasePath) if err != nil { return errors.New("database startup failed") } defer func() { if err := db.Close(); err != nil { log.Print("database close failed") } }() runner, err := migration.New(db) if err != nil { return errors.New("migration setup failed") } if err := requireCurrentMigrations(ctx, runner); err != nil { return err } store, err := repository.New(db) if err != nil { return errors.New("auth repository setup failed") } passwords, err := password.NewBcrypt(12) if err != nil { return errors.New("password manager setup failed") } service, err := usecase.NewAuthService( store, passwords, usecase.SystemClock{}, usecase.UUIDGenerator{}, usecase.CryptoTokenGenerator{}, ) if err != nil { return errors.New("auth service setup failed") } switch command { case "create-user": user, err := service.ProvisionUser( ctx, usecase.ProvisionUserCommand{ Username: value, Password: plainPassword, Role: role, Active: true, }, ) if err != nil { return publicProvisioningError(err) } _, err = fmt.Fprintf( output, "user_id=%s username=%s role=%s\n", user.ID, user.Username, user.Role, ) return err case "create-device": result, err := service.ProvisionDevice( ctx, usecase.ProvisionDeviceCommand{ Name: value, Enabled: true, }, ) if err != nil { return publicProvisioningError(err) } _, err = fmt.Fprintf( output, "device_id=%s\ndevice_token=%s\n", result.Device.ID, result.DeviceToken, ) return err case "reset-password": if err := service.ResetUserPassword( ctx, usecase.ResetUserPasswordCommand{ Username: value, Password: plainPassword, }, ); err != nil { return publicProvisioningError(err) } _, err = fmt.Fprintf( output, "username=%s password_updated=true\n", domain.NormalizeUsername(value), ) return err case "enable-user", "disable-user": active := command == "enable-user" if err := service.SetUserActive( ctx, usecase.SetUserActiveCommand{ Username: value, Active: active, }, ); err != nil { return publicProvisioningError(err) } _, err = fmt.Fprintf( output, "username=%s active=%t\n", domain.NormalizeUsername(value), active, ) return err case "enable-device", "disable-device": enabled := command == "enable-device" if err := service.SetDeviceEnabled( ctx, usecase.SetDeviceEnabledCommand{ DeviceID: value, Enabled: enabled, }, ); err != nil { return publicProvisioningError(err) } _, err = fmt.Fprintf( output, "device_id=%s enabled=%t\n", strings.TrimSpace(value), enabled, ) return err default: return errors.New("unsupported auth command") } } func parseArguments( arguments []string, ) (string, string, domain.UserRole, error) { if len(arguments) == 2 && arguments[0] == "create-device" { return "create-device", arguments[1], "", nil } if len(arguments) == 2 { switch arguments[0] { case "reset-password", "enable-user", "disable-user", "enable-device", "disable-device": return arguments[0], arguments[1], "", nil } } if len(arguments) == 3 && arguments[0] == "create-user" { role := domain.UserRole(strings.ToUpper(arguments[1])) if role != domain.UserRoleAdmin && role != domain.UserRoleBuyer { return "", "", "", errors.New( "role must be ADMIN or BUYER", ) } return "create-user", arguments[2], role, nil } return "", "", "", errors.New( "usage: authctl create-user | " + "authctl reset-password | " + "authctl create-device | " + "authctl | " + "authctl ", ) } type migrationStatusReader interface { Status(context.Context) ([]migration.Status, error) } func requireCurrentMigrations( ctx context.Context, reader migrationStatusReader, ) error { statuses, err := reader.Status(ctx) if err != nil { return errors.New("database migration status failed") } if len(statuses) == 0 { return errors.New("database has no known migrations") } for _, status := range statuses { if !status.Applied { return errors.New( "database migrations are pending; run migrate up", ) } } return nil } func publicProvisioningError(err error) error { var typed *usecase.Error if !errors.As(err, &typed) { return errors.New("authentication provisioning failed") } switch typed.Kind { case usecase.ErrorKindInvalid: return errors.New("authentication input is invalid") case usecase.ErrorKindConflict: return errors.New("authentication resource already exists") case usecase.ErrorKindUnavailable: return errors.New("authentication storage is unavailable") case usecase.ErrorKindNotFound: return errors.New("authentication resource was not found") default: return errors.New("authentication provisioning failed") } }