250 lines
5.7 KiB
Go
250 lines
5.7 KiB
Go
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" {
|
|
var exists bool
|
|
plainPassword, exists = lookup(passwordEnvironment)
|
|
if !exists || plainPassword == "" {
|
|
return errors.New(
|
|
passwordEnvironment + " must be set for create-user",
|
|
)
|
|
}
|
|
}
|
|
|
|
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 "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 "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 <ADMIN|BUYER> <username> | " +
|
|
"authctl create-device <name> | " +
|
|
"authctl <enable-user|disable-user> <username> | " +
|
|
"authctl <enable-device|disable-device> <device-id>",
|
|
)
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|