feat(auth): 实现 TOTP 双因素认证功能
新增功能: - 支持 Google Authenticator 等应用进行 TOTP 二次验证 - 用户可在个人设置中启用/禁用 2FA - 登录时支持 TOTP 验证流程 - 管理后台可全局开关 TOTP 功能 安全增强: - TOTP 密钥使用 AES-256-GCM 加密存储 - 添加 TOTP_ENCRYPTION_KEY 配置项,必须手动配置才能启用功能 - 防止服务重启导致加密密钥变更使用户无法登录 - 验证失败次数限制,防止暴力破解 配置说明: - Docker 部署:在 .env 中设置 TOTP_ENCRYPTION_KEY - 非 Docker 部署:在 config.yaml 中设置 totp.encryption_key - 生成密钥命令:openssl rand -hex 32
This commit is contained in:
@@ -14360,6 +14360,9 @@ type UserMutation struct {
|
||||
status *string
|
||||
username *string
|
||||
notes *string
|
||||
totp_secret_encrypted *string
|
||||
totp_enabled *bool
|
||||
totp_enabled_at *time.Time
|
||||
clearedFields map[string]struct{}
|
||||
api_keys map[int64]struct{}
|
||||
removedapi_keys map[int64]struct{}
|
||||
@@ -14937,6 +14940,140 @@ func (m *UserMutation) ResetNotes() {
|
||||
m.notes = nil
|
||||
}
|
||||
|
||||
// SetTotpSecretEncrypted sets the "totp_secret_encrypted" field.
|
||||
func (m *UserMutation) SetTotpSecretEncrypted(s string) {
|
||||
m.totp_secret_encrypted = &s
|
||||
}
|
||||
|
||||
// TotpSecretEncrypted returns the value of the "totp_secret_encrypted" field in the mutation.
|
||||
func (m *UserMutation) TotpSecretEncrypted() (r string, exists bool) {
|
||||
v := m.totp_secret_encrypted
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldTotpSecretEncrypted returns the old "totp_secret_encrypted" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldTotpSecretEncrypted(ctx context.Context) (v *string, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldTotpSecretEncrypted is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldTotpSecretEncrypted requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldTotpSecretEncrypted: %w", err)
|
||||
}
|
||||
return oldValue.TotpSecretEncrypted, nil
|
||||
}
|
||||
|
||||
// ClearTotpSecretEncrypted clears the value of the "totp_secret_encrypted" field.
|
||||
func (m *UserMutation) ClearTotpSecretEncrypted() {
|
||||
m.totp_secret_encrypted = nil
|
||||
m.clearedFields[user.FieldTotpSecretEncrypted] = struct{}{}
|
||||
}
|
||||
|
||||
// TotpSecretEncryptedCleared returns if the "totp_secret_encrypted" field was cleared in this mutation.
|
||||
func (m *UserMutation) TotpSecretEncryptedCleared() bool {
|
||||
_, ok := m.clearedFields[user.FieldTotpSecretEncrypted]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetTotpSecretEncrypted resets all changes to the "totp_secret_encrypted" field.
|
||||
func (m *UserMutation) ResetTotpSecretEncrypted() {
|
||||
m.totp_secret_encrypted = nil
|
||||
delete(m.clearedFields, user.FieldTotpSecretEncrypted)
|
||||
}
|
||||
|
||||
// SetTotpEnabled sets the "totp_enabled" field.
|
||||
func (m *UserMutation) SetTotpEnabled(b bool) {
|
||||
m.totp_enabled = &b
|
||||
}
|
||||
|
||||
// TotpEnabled returns the value of the "totp_enabled" field in the mutation.
|
||||
func (m *UserMutation) TotpEnabled() (r bool, exists bool) {
|
||||
v := m.totp_enabled
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldTotpEnabled returns the old "totp_enabled" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldTotpEnabled(ctx context.Context) (v bool, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldTotpEnabled is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldTotpEnabled requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldTotpEnabled: %w", err)
|
||||
}
|
||||
return oldValue.TotpEnabled, nil
|
||||
}
|
||||
|
||||
// ResetTotpEnabled resets all changes to the "totp_enabled" field.
|
||||
func (m *UserMutation) ResetTotpEnabled() {
|
||||
m.totp_enabled = nil
|
||||
}
|
||||
|
||||
// SetTotpEnabledAt sets the "totp_enabled_at" field.
|
||||
func (m *UserMutation) SetTotpEnabledAt(t time.Time) {
|
||||
m.totp_enabled_at = &t
|
||||
}
|
||||
|
||||
// TotpEnabledAt returns the value of the "totp_enabled_at" field in the mutation.
|
||||
func (m *UserMutation) TotpEnabledAt() (r time.Time, exists bool) {
|
||||
v := m.totp_enabled_at
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
return *v, true
|
||||
}
|
||||
|
||||
// OldTotpEnabledAt returns the old "totp_enabled_at" field's value of the User entity.
|
||||
// If the User object wasn't provided to the builder, the object is fetched from the database.
|
||||
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
|
||||
func (m *UserMutation) OldTotpEnabledAt(ctx context.Context) (v *time.Time, err error) {
|
||||
if !m.op.Is(OpUpdateOne) {
|
||||
return v, errors.New("OldTotpEnabledAt is only allowed on UpdateOne operations")
|
||||
}
|
||||
if m.id == nil || m.oldValue == nil {
|
||||
return v, errors.New("OldTotpEnabledAt requires an ID field in the mutation")
|
||||
}
|
||||
oldValue, err := m.oldValue(ctx)
|
||||
if err != nil {
|
||||
return v, fmt.Errorf("querying old value for OldTotpEnabledAt: %w", err)
|
||||
}
|
||||
return oldValue.TotpEnabledAt, nil
|
||||
}
|
||||
|
||||
// ClearTotpEnabledAt clears the value of the "totp_enabled_at" field.
|
||||
func (m *UserMutation) ClearTotpEnabledAt() {
|
||||
m.totp_enabled_at = nil
|
||||
m.clearedFields[user.FieldTotpEnabledAt] = struct{}{}
|
||||
}
|
||||
|
||||
// TotpEnabledAtCleared returns if the "totp_enabled_at" field was cleared in this mutation.
|
||||
func (m *UserMutation) TotpEnabledAtCleared() bool {
|
||||
_, ok := m.clearedFields[user.FieldTotpEnabledAt]
|
||||
return ok
|
||||
}
|
||||
|
||||
// ResetTotpEnabledAt resets all changes to the "totp_enabled_at" field.
|
||||
func (m *UserMutation) ResetTotpEnabledAt() {
|
||||
m.totp_enabled_at = nil
|
||||
delete(m.clearedFields, user.FieldTotpEnabledAt)
|
||||
}
|
||||
|
||||
// AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids.
|
||||
func (m *UserMutation) AddAPIKeyIDs(ids ...int64) {
|
||||
if m.api_keys == nil {
|
||||
@@ -15403,7 +15540,7 @@ func (m *UserMutation) Type() string {
|
||||
// order to get all numeric fields that were incremented/decremented, call
|
||||
// AddedFields().
|
||||
func (m *UserMutation) Fields() []string {
|
||||
fields := make([]string, 0, 11)
|
||||
fields := make([]string, 0, 14)
|
||||
if m.created_at != nil {
|
||||
fields = append(fields, user.FieldCreatedAt)
|
||||
}
|
||||
@@ -15437,6 +15574,15 @@ func (m *UserMutation) Fields() []string {
|
||||
if m.notes != nil {
|
||||
fields = append(fields, user.FieldNotes)
|
||||
}
|
||||
if m.totp_secret_encrypted != nil {
|
||||
fields = append(fields, user.FieldTotpSecretEncrypted)
|
||||
}
|
||||
if m.totp_enabled != nil {
|
||||
fields = append(fields, user.FieldTotpEnabled)
|
||||
}
|
||||
if m.totp_enabled_at != nil {
|
||||
fields = append(fields, user.FieldTotpEnabledAt)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -15467,6 +15613,12 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) {
|
||||
return m.Username()
|
||||
case user.FieldNotes:
|
||||
return m.Notes()
|
||||
case user.FieldTotpSecretEncrypted:
|
||||
return m.TotpSecretEncrypted()
|
||||
case user.FieldTotpEnabled:
|
||||
return m.TotpEnabled()
|
||||
case user.FieldTotpEnabledAt:
|
||||
return m.TotpEnabledAt()
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -15498,6 +15650,12 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er
|
||||
return m.OldUsername(ctx)
|
||||
case user.FieldNotes:
|
||||
return m.OldNotes(ctx)
|
||||
case user.FieldTotpSecretEncrypted:
|
||||
return m.OldTotpSecretEncrypted(ctx)
|
||||
case user.FieldTotpEnabled:
|
||||
return m.OldTotpEnabled(ctx)
|
||||
case user.FieldTotpEnabledAt:
|
||||
return m.OldTotpEnabledAt(ctx)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown User field %s", name)
|
||||
}
|
||||
@@ -15584,6 +15742,27 @@ func (m *UserMutation) SetField(name string, value ent.Value) error {
|
||||
}
|
||||
m.SetNotes(v)
|
||||
return nil
|
||||
case user.FieldTotpSecretEncrypted:
|
||||
v, ok := value.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetTotpSecretEncrypted(v)
|
||||
return nil
|
||||
case user.FieldTotpEnabled:
|
||||
v, ok := value.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetTotpEnabled(v)
|
||||
return nil
|
||||
case user.FieldTotpEnabledAt:
|
||||
v, ok := value.(time.Time)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||
}
|
||||
m.SetTotpEnabledAt(v)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User field %s", name)
|
||||
}
|
||||
@@ -15644,6 +15823,12 @@ func (m *UserMutation) ClearedFields() []string {
|
||||
if m.FieldCleared(user.FieldDeletedAt) {
|
||||
fields = append(fields, user.FieldDeletedAt)
|
||||
}
|
||||
if m.FieldCleared(user.FieldTotpSecretEncrypted) {
|
||||
fields = append(fields, user.FieldTotpSecretEncrypted)
|
||||
}
|
||||
if m.FieldCleared(user.FieldTotpEnabledAt) {
|
||||
fields = append(fields, user.FieldTotpEnabledAt)
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
@@ -15661,6 +15846,12 @@ func (m *UserMutation) ClearField(name string) error {
|
||||
case user.FieldDeletedAt:
|
||||
m.ClearDeletedAt()
|
||||
return nil
|
||||
case user.FieldTotpSecretEncrypted:
|
||||
m.ClearTotpSecretEncrypted()
|
||||
return nil
|
||||
case user.FieldTotpEnabledAt:
|
||||
m.ClearTotpEnabledAt()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User nullable field %s", name)
|
||||
}
|
||||
@@ -15702,6 +15893,15 @@ func (m *UserMutation) ResetField(name string) error {
|
||||
case user.FieldNotes:
|
||||
m.ResetNotes()
|
||||
return nil
|
||||
case user.FieldTotpSecretEncrypted:
|
||||
m.ResetTotpSecretEncrypted()
|
||||
return nil
|
||||
case user.FieldTotpEnabled:
|
||||
m.ResetTotpEnabled()
|
||||
return nil
|
||||
case user.FieldTotpEnabledAt:
|
||||
m.ResetTotpEnabledAt()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown User field %s", name)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user