feat(authctl): support password reset
This commit is contained in:
@@ -78,12 +78,15 @@ fixture,默认 APK 不携带真实订单资料。
|
|||||||
Set-Location backend-api
|
Set-Location backend-api
|
||||||
$env:GOTOOLCHAIN = "local"
|
$env:GOTOOLCHAIN = "local"
|
||||||
go run ./cmd/migrate up
|
go run ./cmd/migrate up
|
||||||
$env:CMROUBAO_AUTH_PASSWORD = "至少 6 个 UTF-8 字节"
|
$env:CMROUBAO_AUTH_PASSWORD = "至少 5 个 UTF-8 字节"
|
||||||
go run ./cmd/authctl create-user ADMIN admin
|
go run ./cmd/authctl create-user ADMIN admin
|
||||||
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
$env:CMROUBAO_AUTH_PASSWORD = "采购员独立强密码,至少 6 个 UTF-8 字节"
|
$env:CMROUBAO_AUTH_PASSWORD = "采购员独立强密码,至少 5 个 UTF-8 字节"
|
||||||
go run ./cmd/authctl create-user BUYER buyer01
|
go run ./cmd/authctl create-user BUYER buyer01
|
||||||
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
|
$env:CMROUBAO_AUTH_PASSWORD = "管理员新密码,至少 5 个 UTF-8 字节"
|
||||||
|
go run ./cmd/authctl reset-password admin
|
||||||
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
go run ./cmd/authctl create-device buyer-phone-01
|
go run ./cmd/authctl create-device buyer-phone-01
|
||||||
go run ./cmd/api
|
go run ./cmd/api
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -39,13 +39,16 @@ go build -o bin/cmroubao-authctl.exe ./cmd/authctl
|
|||||||
go run ./cmd/migrate up
|
go run ./cmd/migrate up
|
||||||
go run ./cmd/migrate status
|
go run ./cmd/migrate status
|
||||||
|
|
||||||
$env:CMROUBAO_AUTH_PASSWORD = "至少 6 个 UTF-8 字节"
|
$env:CMROUBAO_AUTH_PASSWORD = "至少 5 个 UTF-8 字节"
|
||||||
go run ./cmd/authctl create-user ADMIN admin
|
go run ./cmd/authctl create-user ADMIN admin
|
||||||
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
|
|
||||||
$env:CMROUBAO_AUTH_PASSWORD = "采购员独立强密码,至少 6 个 UTF-8 字节"
|
$env:CMROUBAO_AUTH_PASSWORD = "采购员独立强密码,至少 5 个 UTF-8 字节"
|
||||||
go run ./cmd/authctl create-user BUYER buyer01
|
go run ./cmd/authctl create-user BUYER buyer01
|
||||||
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
|
$env:CMROUBAO_AUTH_PASSWORD = "账号新密码,至少 5 个 UTF-8 字节"
|
||||||
|
go run ./cmd/authctl reset-password admin
|
||||||
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
go run ./cmd/authctl create-device buyer-phone-01
|
go run ./cmd/authctl create-device buyer-phone-01
|
||||||
# 发生人员离岗或设备风险时,现有凭证会随禁用立即失效
|
# 发生人员离岗或设备风险时,现有凭证会随禁用立即失效
|
||||||
go run ./cmd/authctl disable-user buyer01
|
go run ./cmd/authctl disable-user buyer01
|
||||||
@@ -57,7 +60,8 @@ go run ./cmd/api
|
|||||||
API 启动前会检查全部 migration 已应用;发现 pending migration 会拒绝启动并提示先
|
API 启动前会检查全部 migration 已应用;发现 pending migration 会拒绝启动并提示先
|
||||||
执行 `go run ./cmd/migrate up`,不会在服务进程内自动改表。
|
执行 `go run ./cmd/migrate up`,不会在服务进程内自动改表。
|
||||||
|
|
||||||
`authctl create-user` 的密码只从 `CMROUBAO_AUTH_PASSWORD` 读取,不接受命令行密码。
|
`authctl create-user` 和 `authctl reset-password` 的密码只从
|
||||||
|
`CMROUBAO_AUTH_PASSWORD` 读取,不接受命令行密码。
|
||||||
`create-device` 只在成功时输出一次设备 ID 和 256 bit 设备 token;原值应立即放入
|
`create-device` 只在成功时输出一次设备 ID 和 256 bit 设备 token;原值应立即放入
|
||||||
设备安全配置,不得写入 Git、普通日志或共享文档。`enable-user`、`disable-user`、
|
设备安全配置,不得写入 Git、普通日志或共享文档。`enable-user`、`disable-user`、
|
||||||
`enable-device`、`disable-device` 是受支持的本地停用/恢复入口;禁用会让该主体的
|
`enable-device`、`disable-device` 是受支持的本地停用/恢复入口;禁用会让该主体的
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ func run(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var plainPassword string
|
var plainPassword string
|
||||||
if command == "create-user" {
|
if command == "create-user" || command == "reset-password" {
|
||||||
var exists bool
|
var exists bool
|
||||||
plainPassword, exists = lookup(passwordEnvironment)
|
plainPassword, exists = lookup(passwordEnvironment)
|
||||||
if !exists || plainPassword == "" {
|
if !exists || plainPassword == "" {
|
||||||
return errors.New(
|
return errors.New(
|
||||||
passwordEnvironment + " must be set for create-user",
|
passwordEnvironment + " must be set for " + command,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +133,22 @@ func run(
|
|||||||
result.DeviceToken,
|
result.DeviceToken,
|
||||||
)
|
)
|
||||||
return err
|
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":
|
case "enable-user", "disable-user":
|
||||||
active := command == "enable-user"
|
active := command == "enable-user"
|
||||||
if err := service.SetUserActive(
|
if err := service.SetUserActive(
|
||||||
@@ -182,7 +198,7 @@ func parseArguments(
|
|||||||
}
|
}
|
||||||
if len(arguments) == 2 {
|
if len(arguments) == 2 {
|
||||||
switch arguments[0] {
|
switch arguments[0] {
|
||||||
case "enable-user", "disable-user",
|
case "reset-password", "enable-user", "disable-user",
|
||||||
"enable-device", "disable-device":
|
"enable-device", "disable-device":
|
||||||
return arguments[0], arguments[1], "", nil
|
return arguments[0], arguments[1], "", nil
|
||||||
}
|
}
|
||||||
@@ -198,6 +214,7 @@ func parseArguments(
|
|||||||
}
|
}
|
||||||
return "", "", "", errors.New(
|
return "", "", "", errors.New(
|
||||||
"usage: authctl create-user <ADMIN|BUYER> <username> | " +
|
"usage: authctl create-user <ADMIN|BUYER> <username> | " +
|
||||||
|
"authctl reset-password <username> | " +
|
||||||
"authctl create-device <name> | " +
|
"authctl create-device <name> | " +
|
||||||
"authctl <enable-user|disable-user> <username> | " +
|
"authctl <enable-user|disable-user> <username> | " +
|
||||||
"authctl <enable-device|disable-device> <device-id>",
|
"authctl <enable-device|disable-device> <device-id>",
|
||||||
|
|||||||
@@ -64,6 +64,48 @@ func TestRunCreatesUserWithoutExposingPassword(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunResetsPasswordWithoutExposingIt(t *testing.T) {
|
||||||
|
databasePath := migratedDatabase(t)
|
||||||
|
var output bytes.Buffer
|
||||||
|
if err := run(
|
||||||
|
[]string{"create-user", "ADMIN", "Admin"},
|
||||||
|
testLookup(databasePath, "old-password"),
|
||||||
|
&output,
|
||||||
|
); err != nil {
|
||||||
|
t.Fatalf("create user: %v", err)
|
||||||
|
}
|
||||||
|
output.Reset()
|
||||||
|
|
||||||
|
err := run(
|
||||||
|
[]string{"reset-password", "ADMIN"},
|
||||||
|
testLookup(databasePath, "admin"),
|
||||||
|
&output,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run(reset-password) error = %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(output.String(), "password=admin") ||
|
||||||
|
output.String() != "username=admin password_updated=true\n" {
|
||||||
|
t.Fatalf("output = %q", output.String())
|
||||||
|
}
|
||||||
|
db, err := database.Open(context.Background(), databasePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("database.Open() error = %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
var hash string
|
||||||
|
if err := db.QueryRow(
|
||||||
|
"SELECT password_hash FROM users WHERE username = 'admin'",
|
||||||
|
).Scan(&hash); err != nil {
|
||||||
|
t.Fatalf("query user: %v", err)
|
||||||
|
}
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(hash), []byte("admin")) != nil ||
|
||||||
|
bcrypt.CompareHashAndPassword([]byte(hash), []byte("old-password")) == nil {
|
||||||
|
t.Fatal("password was not replaced with a bcrypt hash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunCreatesDeviceAndStoresOnlyTokenHash(t *testing.T) {
|
func TestRunCreatesDeviceAndStoresOnlyTokenHash(t *testing.T) {
|
||||||
databasePath := migratedDatabase(t)
|
databasePath := migratedDatabase(t)
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
@@ -177,7 +219,7 @@ func TestRunRejectsWeakPasswordAndInvalidCommands(t *testing.T) {
|
|||||||
databasePath := migratedDatabase(t)
|
databasePath := migratedDatabase(t)
|
||||||
err := run(
|
err := run(
|
||||||
[]string{"create-user", "BUYER", "buyer"},
|
[]string{"create-user", "BUYER", "buyer"},
|
||||||
testLookup(databasePath, "short"),
|
testLookup(databasePath, "tiny"),
|
||||||
&bytes.Buffer{},
|
&bytes.Buffer{},
|
||||||
)
|
)
|
||||||
if err == nil || err.Error() != "authentication input is invalid" {
|
if err == nil || err.Error() != "authentication input is invalid" {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const (
|
|||||||
MaxUsernameBytes = 128
|
MaxUsernameBytes = 128
|
||||||
MaxDeviceNameBytes = 128
|
MaxDeviceNameBytes = 128
|
||||||
MaxVersionBytes = 128
|
MaxVersionBytes = 128
|
||||||
MinPasswordBytes = 6
|
MinPasswordBytes = 5
|
||||||
MaxPasswordBytes = 72
|
MaxPasswordBytes = 72
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestNormalizeUsernameAndValidateUserInput(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
"blank username": {" ", "password", UserRoleAdmin},
|
"blank username": {" ", "password", UserRoleAdmin},
|
||||||
"blank password": {"admin", "", UserRoleAdmin},
|
"blank password": {"admin", "", UserRoleAdmin},
|
||||||
"short password": {"admin", "short", UserRoleAdmin},
|
"short password": {"admin", "tiny", UserRoleAdmin},
|
||||||
"invalid password UTF-8": {
|
"invalid password UTF-8": {
|
||||||
"admin",
|
"admin",
|
||||||
string([]byte{0xff, 0xfe, 0xfd}),
|
string([]byte{0xff, 0xfe, 0xfd}),
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ func TestBcryptRejectsInvalidCostAndOversizedPasswords(t *testing.T) {
|
|||||||
t.Fatalf("NewBcrypt() error = %v", err)
|
t.Fatalf("NewBcrypt() error = %v", err)
|
||||||
}
|
}
|
||||||
oversized := strings.Repeat("x", 73)
|
oversized := strings.Repeat("x", 73)
|
||||||
if _, err := manager.Hash("short"); !errors.Is(
|
if _, err := manager.Hash("tiny"); !errors.Is(
|
||||||
err,
|
err,
|
||||||
ErrInvalidPassword,
|
ErrInvalidPassword,
|
||||||
) {
|
) {
|
||||||
t.Fatalf("Hash(short) error = %v", err)
|
t.Fatalf("Hash(short) error = %v", err)
|
||||||
}
|
}
|
||||||
if _, err := manager.Hash("sixsix"); err != nil {
|
if _, err := manager.Hash("admin"); err != nil {
|
||||||
t.Fatalf("Hash(six-byte password) error = %v", err)
|
t.Fatalf("Hash(five-byte password) error = %v", err)
|
||||||
}
|
}
|
||||||
if _, err := manager.Hash(oversized); !errors.Is(
|
if _, err := manager.Hash(oversized); !errors.Is(
|
||||||
err,
|
err,
|
||||||
|
|||||||
@@ -95,6 +95,24 @@ func (s *Store) SetUserActive(
|
|||||||
return requireAffectedAuthResource(result, err)
|
return requireAffectedAuthResource(result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) ResetUserPassword(
|
||||||
|
ctx context.Context,
|
||||||
|
username string,
|
||||||
|
passwordHash string,
|
||||||
|
updatedAt time.Time,
|
||||||
|
) error {
|
||||||
|
result, err := s.db.ExecContext(
|
||||||
|
ctx,
|
||||||
|
`UPDATE users
|
||||||
|
SET password_hash = ?, updated_at = ?
|
||||||
|
WHERE username = ?`,
|
||||||
|
passwordHash,
|
||||||
|
formatTimestamp(updatedAt),
|
||||||
|
username,
|
||||||
|
)
|
||||||
|
return requireAffectedAuthResource(result, err)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) SetDeviceEnabled(
|
func (s *Store) SetDeviceEnabled(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
deviceID string,
|
deviceID string,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type AuthRepository interface {
|
|||||||
FindUserByUsername(context.Context, string) (domain.User, error)
|
FindUserByUsername(context.Context, string) (domain.User, error)
|
||||||
ProvisionUser(context.Context, domain.User) (domain.User, error)
|
ProvisionUser(context.Context, domain.User) (domain.User, error)
|
||||||
ProvisionDevice(context.Context, domain.Device) (domain.Device, error)
|
ProvisionDevice(context.Context, domain.Device) (domain.Device, error)
|
||||||
|
ResetUserPassword(context.Context, string, string, time.Time) error
|
||||||
SetUserActive(context.Context, string, bool, time.Time) error
|
SetUserActive(context.Context, string, bool, time.Time) error
|
||||||
SetDeviceEnabled(context.Context, string, bool, time.Time) error
|
SetDeviceEnabled(context.Context, string, bool, time.Time) error
|
||||||
CreateAdminSession(
|
CreateAdminSession(
|
||||||
|
|||||||
@@ -74,6 +74,11 @@ type SetUserActiveCommand struct {
|
|||||||
Active bool
|
Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ResetUserPasswordCommand struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
type SetDeviceEnabledCommand struct {
|
type SetDeviceEnabledCommand struct {
|
||||||
DeviceID string
|
DeviceID string
|
||||||
Enabled bool
|
Enabled bool
|
||||||
@@ -396,6 +401,49 @@ func (s *AuthService) SetUserActive(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) ResetUserPassword(
|
||||||
|
ctx context.Context,
|
||||||
|
command ResetUserPasswordCommand,
|
||||||
|
) error {
|
||||||
|
username := domain.NormalizeUsername(command.Username)
|
||||||
|
user, err := s.repository.FindUserByUsername(ctx, username)
|
||||||
|
if err != nil {
|
||||||
|
return wrapAuthRepositoryError(err)
|
||||||
|
}
|
||||||
|
if err := domain.ValidateUserInput(
|
||||||
|
username,
|
||||||
|
command.Password,
|
||||||
|
user.Role,
|
||||||
|
); err != nil {
|
||||||
|
var validation *domain.AuthValidationError
|
||||||
|
if errors.As(err, &validation) {
|
||||||
|
return invalidError(
|
||||||
|
"AUTH_VALIDATION_FAILED",
|
||||||
|
"authentication input is invalid",
|
||||||
|
validation.Fields,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return invalidError(
|
||||||
|
"AUTH_VALIDATION_FAILED",
|
||||||
|
"authentication input is invalid",
|
||||||
|
map[string]string{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
passwordHash, err := s.passwords.Hash(command.Password)
|
||||||
|
if err != nil {
|
||||||
|
return internalAuthFailure(err)
|
||||||
|
}
|
||||||
|
if err := s.repository.ResetUserPassword(
|
||||||
|
ctx,
|
||||||
|
username,
|
||||||
|
passwordHash,
|
||||||
|
s.clock.Now().UTC(),
|
||||||
|
); err != nil {
|
||||||
|
return wrapAuthRepositoryError(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AuthService) SetDeviceEnabled(
|
func (s *AuthService) SetDeviceEnabled(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
command SetDeviceEnabledCommand,
|
command SetDeviceEnabledCommand,
|
||||||
|
|||||||
@@ -213,6 +213,41 @@ func TestAuthServiceProvisionsHashedCredentials(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthServiceResetsExistingUserPassword(t *testing.T) {
|
||||||
|
fixture := newAuthServiceFixture(t)
|
||||||
|
fixture.repository.users["admin"] = domain.User{
|
||||||
|
ID: "user-admin",
|
||||||
|
Username: "admin",
|
||||||
|
PasswordHash: "hashed:old-password",
|
||||||
|
Role: domain.UserRoleAdmin,
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fixture.service.ResetUserPassword(
|
||||||
|
context.Background(),
|
||||||
|
ResetUserPasswordCommand{
|
||||||
|
Username: " ADMIN ",
|
||||||
|
Password: "admin",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResetUserPassword() error = %v", err)
|
||||||
|
}
|
||||||
|
if fixture.repository.users["admin"].PasswordHash != "hashed:admin" {
|
||||||
|
t.Fatalf(
|
||||||
|
"password hash = %q",
|
||||||
|
fixture.repository.users["admin"].PasswordHash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fixture.service.ResetUserPassword(
|
||||||
|
context.Background(),
|
||||||
|
ResetUserPasswordCommand{Username: "missing", Password: "admin"},
|
||||||
|
)
|
||||||
|
assertAuthCode(t, err, "RESOURCE_NOT_FOUND")
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthServiceChangesUserAndDeviceStatus(t *testing.T) {
|
func TestAuthServiceChangesUserAndDeviceStatus(t *testing.T) {
|
||||||
fixture := newAuthServiceFixture(t)
|
fixture := newAuthServiceFixture(t)
|
||||||
if err := fixture.service.SetUserActive(
|
if err := fixture.service.SetUserActive(
|
||||||
@@ -409,6 +444,21 @@ func (repository *fakeAuthRepository) ProvisionDevice(
|
|||||||
return device, nil
|
return device, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repository *fakeAuthRepository) ResetUserPassword(
|
||||||
|
_ context.Context,
|
||||||
|
username string,
|
||||||
|
passwordHash string,
|
||||||
|
_ time.Time,
|
||||||
|
) error {
|
||||||
|
user, found := repository.users[username]
|
||||||
|
if !found {
|
||||||
|
return ErrRepositoryNotFound
|
||||||
|
}
|
||||||
|
user.PasswordHash = passwordHash
|
||||||
|
repository.users[username] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (repository *fakeAuthRepository) SetUserActive(
|
func (repository *fakeAuthRepository) SetUserActive(
|
||||||
_ context.Context,
|
_ context.Context,
|
||||||
username string,
|
username string,
|
||||||
|
|||||||
@@ -126,8 +126,11 @@ $env:RUN_START_COMMAND = "1"
|
|||||||
Set-Location backend-api
|
Set-Location backend-api
|
||||||
$env:GOTOOLCHAIN = "local"
|
$env:GOTOOLCHAIN = "local"
|
||||||
go run ./cmd/migrate up
|
go run ./cmd/migrate up
|
||||||
$env:CMROUBAO_AUTH_PASSWORD = "至少 6 个 UTF-8 字节"
|
$env:CMROUBAO_AUTH_PASSWORD = "至少 5 个 UTF-8 字节"
|
||||||
go run ./cmd/authctl create-user ADMIN admin
|
go run ./cmd/authctl create-user ADMIN admin
|
||||||
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
|
$env:CMROUBAO_AUTH_PASSWORD = "账号新密码,至少 5 个 UTF-8 字节"
|
||||||
|
go run ./cmd/authctl reset-password admin
|
||||||
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
go run ./cmd/api
|
go run ./cmd/api
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -200,9 +200,12 @@ $env:RUN_START_COMMAND = "1"
|
|||||||
Set-Location backend-api
|
Set-Location backend-api
|
||||||
$env:GOTOOLCHAIN = "local"
|
$env:GOTOOLCHAIN = "local"
|
||||||
go run ./cmd/migrate up
|
go run ./cmd/migrate up
|
||||||
$env:CMROUBAO_AUTH_PASSWORD = "至少 6 个 UTF-8 字节"
|
$env:CMROUBAO_AUTH_PASSWORD = "至少 5 个 UTF-8 字节"
|
||||||
go run ./cmd/authctl create-user ADMIN admin
|
go run ./cmd/authctl create-user ADMIN admin
|
||||||
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
|
$env:CMROUBAO_AUTH_PASSWORD = "账号新密码,至少 5 个 UTF-8 字节"
|
||||||
|
go run ./cmd/authctl reset-password admin
|
||||||
|
Remove-Item Env:CMROUBAO_AUTH_PASSWORD
|
||||||
go run ./cmd/api
|
go run ./cmd/api
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user