feat: implement api for "forgot password?" flow (#14915)

Relates to https://github.com/coder/coder/issues/14232

This implements two endpoints (names subject to change):
- `/api/v2/users/otp/request`
- `/api/v2/users/otp/change-password`
This commit is contained in:
Danielle Maywood
2024-10-04 11:53:25 +01:00
committed by GitHub
parent 8785a51b09
commit 4369f2b4b5
25 changed files with 1007 additions and 4 deletions
+8
View File
@@ -3628,6 +3628,14 @@ func (q *querier) UpdateUserGithubComUserID(ctx context.Context, arg database.Up
return q.db.UpdateUserGithubComUserID(ctx, arg)
}
func (q *querier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg database.UpdateUserHashedOneTimePasscodeParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
return err
}
return q.db.UpdateUserHashedOneTimePasscode(ctx, arg)
}
func (q *querier) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
user, err := q.db.GetUserByID(ctx, arg.ID)
if err != nil {
+6
View File
@@ -1187,6 +1187,12 @@ func (s *MethodTestSuite) TestUser() {
ID: u.ID,
}).Asserts(u, policy.ActionUpdatePersonal).Returns()
}))
s.Run("UpdateUserHashedOneTimePasscode", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserHashedOneTimePasscodeParams{
ID: u.ID,
}).Asserts(rbac.ResourceSystem, policy.ActionUpdate).Returns()
}))
s.Run("UpdateUserQuietHoursSchedule", s.Subtest(func(db database.Store, check *expects) {
u := dbgen.User(s.T(), db, database.User{})
check.Args(database.UpdateUserQuietHoursScheduleParams{
+22
View File
@@ -9077,6 +9077,26 @@ func (q *FakeQuerier) UpdateUserGithubComUserID(_ context.Context, arg database.
return sql.ErrNoRows
}
func (q *FakeQuerier) UpdateUserHashedOneTimePasscode(_ context.Context, arg database.UpdateUserHashedOneTimePasscodeParams) error {
err := validateDatabaseType(arg)
if err != nil {
return err
}
q.mutex.Lock()
defer q.mutex.Unlock()
for i, user := range q.users {
if user.ID != arg.ID {
continue
}
user.HashedOneTimePasscode = arg.HashedOneTimePasscode
user.OneTimePasscodeExpiresAt = arg.OneTimePasscodeExpiresAt
q.users[i] = user
}
return nil
}
func (q *FakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.UpdateUserHashedPasswordParams) error {
if err := validateDatabaseType(arg); err != nil {
return err
@@ -9090,6 +9110,8 @@ func (q *FakeQuerier) UpdateUserHashedPassword(_ context.Context, arg database.U
continue
}
user.HashedPassword = arg.HashedPassword
user.HashedOneTimePasscode = nil
user.OneTimePasscodeExpiresAt = sql.NullTime{}
q.users[i] = user
return nil
}
+7
View File
@@ -2307,6 +2307,13 @@ func (m metricsStore) UpdateUserGithubComUserID(ctx context.Context, arg databas
return r0
}
func (m metricsStore) UpdateUserHashedOneTimePasscode(ctx context.Context, arg database.UpdateUserHashedOneTimePasscodeParams) error {
start := time.Now()
r0 := m.s.UpdateUserHashedOneTimePasscode(ctx, arg)
m.queryLatencies.WithLabelValues("UpdateUserHashedOneTimePasscode").Observe(time.Since(start).Seconds())
return r0
}
func (m metricsStore) UpdateUserHashedPassword(ctx context.Context, arg database.UpdateUserHashedPasswordParams) error {
start := time.Now()
err := m.s.UpdateUserHashedPassword(ctx, arg)
+14
View File
@@ -4861,6 +4861,20 @@ func (mr *MockStoreMockRecorder) UpdateUserGithubComUserID(arg0, arg1 any) *gomo
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserGithubComUserID", reflect.TypeOf((*MockStore)(nil).UpdateUserGithubComUserID), arg0, arg1)
}
// UpdateUserHashedOneTimePasscode mocks base method.
func (m *MockStore) UpdateUserHashedOneTimePasscode(arg0 context.Context, arg1 database.UpdateUserHashedOneTimePasscodeParams) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUserHashedOneTimePasscode", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateUserHashedOneTimePasscode indicates an expected call of UpdateUserHashedOneTimePasscode.
func (mr *MockStoreMockRecorder) UpdateUserHashedOneTimePasscode(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserHashedOneTimePasscode", reflect.TypeOf((*MockStore)(nil).UpdateUserHashedOneTimePasscode), arg0, arg1)
}
// UpdateUserHashedPassword mocks base method.
func (m *MockStore) UpdateUserHashedPassword(arg0 context.Context, arg1 database.UpdateUserHashedPasswordParams) error {
m.ctrl.T.Helper()
@@ -0,0 +1 @@
DELETE FROM notification_templates WHERE id = '62f86a30-2330-4b61-a26d-311ff3b608cf';
@@ -0,0 +1,4 @@
INSERT INTO notification_templates (id, name, title_template, body_template, "group", actions)
VALUES ('62f86a30-2330-4b61-a26d-311ff3b608cf', 'One-Time Passcode', E'Your One-Time Passcode for Coder.',
E'Hi {{.UserName}},\n\nA request to reset the password for your Coder account has been made. Your one-time passcode is:\n\n**{{.Labels.one_time_passcode}}**\n\nIf you did not request to reset your password, you can ignore this message.',
'User Events', '[]'::jsonb);
+1
View File
@@ -457,6 +457,7 @@ type sqlcQuerier interface {
UpdateUserAppearanceSettings(ctx context.Context, arg UpdateUserAppearanceSettingsParams) (User, error)
UpdateUserDeletedByID(ctx context.Context, id uuid.UUID) error
UpdateUserGithubComUserID(ctx context.Context, arg UpdateUserGithubComUserIDParams) error
UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error
UpdateUserHashedPassword(ctx context.Context, arg UpdateUserHashedPasswordParams) error
UpdateUserLastSeenAt(ctx context.Context, arg UpdateUserLastSeenAtParams) (User, error)
UpdateUserLink(ctx context.Context, arg UpdateUserLinkParams) (UserLink, error)
+24 -1
View File
@@ -10528,11 +10528,34 @@ func (q *sqlQuerier) UpdateUserGithubComUserID(ctx context.Context, arg UpdateUs
return err
}
const updateUserHashedOneTimePasscode = `-- name: UpdateUserHashedOneTimePasscode :exec
UPDATE
users
SET
hashed_one_time_passcode = $2,
one_time_passcode_expires_at = $3
WHERE
id = $1
`
type UpdateUserHashedOneTimePasscodeParams struct {
ID uuid.UUID `db:"id" json:"id"`
HashedOneTimePasscode []byte `db:"hashed_one_time_passcode" json:"hashed_one_time_passcode"`
OneTimePasscodeExpiresAt sql.NullTime `db:"one_time_passcode_expires_at" json:"one_time_passcode_expires_at"`
}
func (q *sqlQuerier) UpdateUserHashedOneTimePasscode(ctx context.Context, arg UpdateUserHashedOneTimePasscodeParams) error {
_, err := q.db.ExecContext(ctx, updateUserHashedOneTimePasscode, arg.ID, arg.HashedOneTimePasscode, arg.OneTimePasscodeExpiresAt)
return err
}
const updateUserHashedPassword = `-- name: UpdateUserHashedPassword :exec
UPDATE
users
SET
hashed_password = $2
hashed_password = $2,
hashed_one_time_passcode = NULL,
one_time_passcode_expires_at = NULL
WHERE
id = $1
`
+13 -1
View File
@@ -117,7 +117,9 @@ RETURNING *;
UPDATE
users
SET
hashed_password = $2
hashed_password = $2,
hashed_one_time_passcode = NULL,
one_time_passcode_expires_at = NULL
WHERE
id = $1;
@@ -289,3 +291,13 @@ RETURNING id, email, last_seen_at;
-- AllUserIDs returns all UserIDs regardless of user status or deletion.
-- name: AllUserIDs :many
SELECT DISTINCT id FROM USERS;
-- name: UpdateUserHashedOneTimePasscode :exec
UPDATE
users
SET
hashed_one_time_passcode = $2,
one_time_passcode_expires_at = $3
WHERE
id = $1
;