-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathusers.go
More file actions
1015 lines (905 loc) · 35.8 KB
/
users.go
File metadata and controls
1015 lines (905 loc) · 35.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package codersdk
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
)
// Me is used as a replacement for your own ID.
var Me = "me"
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusDormant UserStatus = "dormant"
UserStatusSuspended UserStatus = "suspended"
)
type UsersRequest struct {
Search string `json:"search,omitempty" typescript:"-"`
Name string `json:"name,omitempty" typescript:"-"`
// Filter users by status.
Status UserStatus `json:"status,omitempty" typescript:"-"`
// Filter users that have the given role.
Role string `json:"role,omitempty" typescript:"-"`
LoginType []LoginType `json:"login_type,omitempty" typescript:"-"`
SearchQuery string `json:"q,omitempty"`
Pagination
}
func (req UsersRequest) asRequestOption() RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
var params []string
if req.Search != "" {
params = append(params, req.Search)
}
if req.Name != "" {
params = append(params, "name:"+req.Name)
}
if req.Status != "" {
params = append(params, "status:"+string(req.Status))
}
if req.Role != "" {
params = append(params, "role:"+req.Role)
}
if req.SearchQuery != "" {
params = append(params, req.SearchQuery)
}
for _, lt := range req.LoginType {
params = append(params, "login_type:"+string(lt))
}
q.Set("q", strings.Join(params, " "))
r.URL.RawQuery = q.Encode()
}
}
// MinimalUser is the minimal information needed to identify a user and show
// them on the UI.
type MinimalUser struct {
ID uuid.UUID `json:"id" validate:"required" table:"id" format:"uuid"`
Username string `json:"username" validate:"required" table:"username,default_sort"`
Name string `json:"name,omitempty" table:"name"`
AvatarURL string `json:"avatar_url,omitempty" format:"uri"`
}
// ReducedUser omits role and organization information. Roles are deduced from
// the user's site and organization roles. This requires fetching the user's
// organizational memberships. Fetching that is more expensive, and not usually
// required by the frontend.
type ReducedUser struct {
MinimalUser `table:"m,recursive_inline"`
Email string `json:"email" validate:"required" table:"email" format:"email"`
CreatedAt time.Time `json:"created_at" validate:"required" table:"created at" format:"date-time"`
UpdatedAt time.Time `json:"updated_at" table:"updated at" format:"date-time"`
LastSeenAt time.Time `json:"last_seen_at,omitempty" format:"date-time"`
Status UserStatus `json:"status" table:"status" enums:"active,suspended"`
LoginType LoginType `json:"login_type"`
IsServiceAccount bool `json:"is_service_account,omitempty"`
// Deprecated: this value should be retrieved from
// `codersdk.UserPreferenceSettings` instead.
ThemePreference string `json:"theme_preference,omitempty"`
}
// User represents a user in Coder.
type User struct {
ReducedUser `table:"r,recursive_inline"`
OrganizationIDs []uuid.UUID `json:"organization_ids" format:"uuid"`
Roles []SlimRole `json:"roles"`
// HasAISeat intentionally omits omitempty so the API always includes the
// field, even when false.
HasAISeat bool `json:"has_ai_seat"`
}
type GetUsersResponse struct {
Users []User `json:"users"`
Count int `json:"count"`
}
// @typescript-ignore LicensorTrialRequest
type LicensorTrialRequest struct {
DeploymentID string `json:"deployment_id"`
Email string `json:"email"`
Source string `json:"source"`
// Personal details.
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
JobTitle string `json:"job_title"`
CompanyName string `json:"company_name"`
Country string `json:"country"`
Developers string `json:"developers"`
}
type CreateFirstUserRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
Name string `json:"name" validate:"user_real_name"`
Password string `json:"password" validate:"required"`
Trial bool `json:"trial"`
TrialInfo CreateFirstUserTrialInfo `json:"trial_info"`
OnboardingInfo *CreateFirstUserOnboardingInfo `json:"onboarding_info,omitempty"`
}
type CreateFirstUserTrialInfo struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
JobTitle string `json:"job_title"`
CompanyName string `json:"company_name"`
Country string `json:"country"`
Developers string `json:"developers"`
}
// CreateFirstUserOnboardingInfo contains optional newsletter preference
// data collected during first user setup.
type CreateFirstUserOnboardingInfo struct {
NewsletterMarketing bool `json:"newsletter_marketing"`
NewsletterReleases bool `json:"newsletter_releases"`
}
// CreateFirstUserResponse contains IDs for newly created user info.
type CreateFirstUserResponse struct {
UserID uuid.UUID `json:"user_id" format:"uuid"`
OrganizationID uuid.UUID `json:"organization_id" format:"uuid"`
}
// CreateUserRequest
// Deprecated: Use CreateUserRequestWithOrgs instead. This will be removed.
// TODO: When removing, we should rename CreateUserRequestWithOrgs -> CreateUserRequest
// Then alias CreateUserRequestWithOrgs to CreateUserRequest.
// @typescript-ignore CreateUserRequest
type CreateUserRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
Username string `json:"username" validate:"required,username"`
Name string `json:"name" validate:"user_real_name"`
Password string `json:"password"`
// UserLoginType defaults to LoginTypePassword.
UserLoginType LoginType `json:"login_type"`
// DisableLogin sets the user's login type to 'none'. This prevents the user
// from being able to use a password or any other authentication method to login.
// Deprecated: Set UserLoginType=LoginTypeDisabled instead.
DisableLogin bool `json:"disable_login"`
OrganizationID uuid.UUID `json:"organization_id" validate:"" format:"uuid"`
}
type CreateUserRequestWithOrgs struct {
Email string `json:"email" validate:"required_unless=ServiceAccount true,omitempty,email" format:"email"`
Username string `json:"username" validate:"required,username"`
Name string `json:"name" validate:"user_real_name"`
Password string `json:"password"`
// UserLoginType defaults to LoginTypePassword.
UserLoginType LoginType `json:"login_type"`
// UserStatus defaults to UserStatusDormant.
UserStatus *UserStatus `json:"user_status"`
// OrganizationIDs is a list of organization IDs that the user should be a member of.
OrganizationIDs []uuid.UUID `json:"organization_ids" validate:"" format:"uuid"`
// Service accounts are admin-managed accounts that cannot login.
ServiceAccount bool `json:"service_account,omitempty"`
}
// UnmarshalJSON implements the unmarshal for the legacy param "organization_id".
// To accommodate multiple organizations, the field has been switched to a slice.
// The previous field will just be appended to the slice.
// Note in the previous behavior, omitting the field would result in the
// default org being applied, but that is no longer the case.
// TODO: Remove this method in it's entirety after some period of time.
// This will be released in v1.16.0, and is associated with the multiple orgs
// feature.
func (r *CreateUserRequestWithOrgs) UnmarshalJSON(data []byte) error {
// By using a type alias, we prevent an infinite recursion when unmarshalling.
// This allows us to use the default unmarshal behavior of the original type.
type AliasedReq CreateUserRequestWithOrgs
type DeprecatedCreateUserRequest struct {
AliasedReq
OrganizationID *uuid.UUID `json:"organization_id" format:"uuid"`
}
var dep DeprecatedCreateUserRequest
err := json.Unmarshal(data, &dep)
if err != nil {
return err
}
*r = CreateUserRequestWithOrgs(dep.AliasedReq)
if dep.OrganizationID != nil {
r.OrganizationIDs = append(r.OrganizationIDs, *dep.OrganizationID)
}
return nil
}
type UpdateUserProfileRequest struct {
Username string `json:"username" validate:"required,username"`
Name string `json:"name" validate:"user_real_name"`
}
type ValidateUserPasswordRequest struct {
Password string `json:"password" validate:"required"`
}
type ValidateUserPasswordResponse struct {
Valid bool `json:"valid"`
Details string `json:"details"`
}
// TerminalFontName is the name of supported terminal font
type TerminalFontName string
var TerminalFontNames = []TerminalFontName{
TerminalFontUnknown, TerminalFontGeistMono, TerminalFontIBMPlexMono,
TerminalFontFiraCode, TerminalFontSourceCodePro, TerminalFontJetBrainsMono,
}
const (
TerminalFontUnknown TerminalFontName = ""
TerminalFontGeistMono TerminalFontName = "geist-mono"
TerminalFontIBMPlexMono TerminalFontName = "ibm-plex-mono"
TerminalFontFiraCode TerminalFontName = "fira-code"
TerminalFontSourceCodePro TerminalFontName = "source-code-pro"
TerminalFontJetBrainsMono TerminalFontName = "jetbrains-mono"
)
type UserAppearanceSettings struct {
ThemePreference string `json:"theme_preference"`
TerminalFont TerminalFontName `json:"terminal_font"`
}
type UpdateUserAppearanceSettingsRequest struct {
ThemePreference string `json:"theme_preference" validate:"required"`
TerminalFont TerminalFontName `json:"terminal_font" validate:"required"`
}
type UserPreferenceSettings struct {
TaskNotificationAlertDismissed bool `json:"task_notification_alert_dismissed"`
ThinkingDisplayMode ThinkingDisplayMode `json:"thinking_display_mode"`
}
type UpdateUserPreferenceSettingsRequest struct {
TaskNotificationAlertDismissed *bool `json:"task_notification_alert_dismissed,omitempty"`
ThinkingDisplayMode ThinkingDisplayMode `json:"thinking_display_mode,omitempty"`
}
type ThinkingDisplayMode string
const (
ThinkingDisplayModeAuto ThinkingDisplayMode = "auto"
ThinkingDisplayModePreview ThinkingDisplayMode = "preview"
ThinkingDisplayModeAlwaysExpanded ThinkingDisplayMode = "always_expanded"
ThinkingDisplayModeAlwaysCollapsed ThinkingDisplayMode = "always_collapsed"
)
var ValidThinkingDisplayModes = []ThinkingDisplayMode{
ThinkingDisplayModeAuto,
ThinkingDisplayModePreview,
ThinkingDisplayModeAlwaysExpanded,
ThinkingDisplayModeAlwaysCollapsed,
}
type UpdateUserPasswordRequest struct {
OldPassword string `json:"old_password" validate:""`
Password string `json:"password" validate:"required"`
}
type UserQuietHoursScheduleResponse struct {
RawSchedule string `json:"raw_schedule"`
// UserSet is true if the user has set their own quiet hours schedule. If
// false, the user is using the default schedule.
UserSet bool `json:"user_set"`
// UserCanSet is true if the user is allowed to set their own quiet hours
// schedule. If false, the user cannot set a custom schedule and the default
// schedule will always be used.
UserCanSet bool `json:"user_can_set"`
// Time is the time of day that the quiet hours window starts in the given
// Timezone each day.
Time string `json:"time"` // HH:mm (24-hour)
Timezone string `json:"timezone"` // raw format from the cron expression, UTC if unspecified
// Next is the next time that the quiet hours window will start.
Next time.Time `json:"next" format:"date-time"`
}
type UpdateUserQuietHoursScheduleRequest struct {
// Schedule is a cron expression that defines when the user's quiet hours
// window is. Schedule must not be empty. For new users, the schedule is set
// to 2am in their browser or computer's timezone. The schedule denotes the
// beginning of a 4 hour window where the workspace is allowed to
// automatically stop or restart due to maintenance or template schedule.
//
// The schedule must be daily with a single time, and should have a timezone
// specified via a CRON_TZ prefix (otherwise UTC will be used).
//
// If the schedule is empty, the user will be updated to use the default
// schedule.
Schedule string `json:"schedule" validate:"required"`
}
type UpdateRoles struct {
Roles []string `json:"roles" validate:""`
}
type UserRoles struct {
Roles []string `json:"roles"`
OrganizationRoles map[uuid.UUID][]string `json:"organization_roles"`
}
type ConvertLoginRequest struct {
// ToType is the login type to convert to.
ToType LoginType `json:"to_type" validate:"required"`
Password string `json:"password" validate:"required"`
}
// LoginWithPasswordRequest enables callers to authenticate with email and password.
type LoginWithPasswordRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
Password string `json:"password" validate:"required"`
}
// LoginWithPasswordResponse contains a session token for the newly authenticated user.
type LoginWithPasswordResponse struct {
SessionToken string `json:"session_token" validate:"required"`
}
// RequestOneTimePasscodeRequest enables callers to request a one-time-passcode to change their password.
type RequestOneTimePasscodeRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
}
// ChangePasswordWithOneTimePasscodeRequest enables callers to change their password when they've forgotten it.
type ChangePasswordWithOneTimePasscodeRequest struct {
Email string `json:"email" validate:"required,email" format:"email"`
Password string `json:"password" validate:"required"`
OneTimePasscode string `json:"one_time_passcode" validate:"required"`
}
type OAuthConversionResponse struct {
StateString string `json:"state_string"`
ExpiresAt time.Time `json:"expires_at" format:"date-time"`
ToType LoginType `json:"to_type"`
UserID uuid.UUID `json:"user_id" format:"uuid"`
}
// AuthMethods contains authentication method information like whether they are enabled or not or custom text, etc.
type AuthMethods struct {
TermsOfServiceURL string `json:"terms_of_service_url,omitempty"`
Password AuthMethod `json:"password"`
Github GithubAuthMethod `json:"github"`
OIDC OIDCAuthMethod `json:"oidc"`
}
type AuthMethod struct {
Enabled bool `json:"enabled"`
}
type UserLoginType struct {
LoginType LoginType `json:"login_type"`
}
type GithubAuthMethod struct {
Enabled bool `json:"enabled"`
DefaultProviderConfigured bool `json:"default_provider_configured"`
}
type OIDCAuthMethod struct {
AuthMethod
SignInText string `json:"signInText"`
IconURL string `json:"iconUrl"`
}
// OIDCClaimsResponse represents the merged OIDC claims for a user.
type OIDCClaimsResponse struct {
// Claims are the merged claims from the OIDC provider. These
// are the union of the ID token claims and the userinfo claims,
// where userinfo claims take precedence on conflict.
Claims map[string]interface{} `json:"claims"`
}
type UserParameter struct {
Name string `json:"name"`
Value string `json:"value"`
}
// UserAutofillParameters returns all recently used parameters for the given user.
func (c *Client) UserAutofillParameters(ctx context.Context, user string, templateID uuid.UUID) ([]UserParameter, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/autofill-parameters?template_id=%s", user, templateID), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var params []UserParameter
return params, json.NewDecoder(res.Body).Decode(¶ms)
}
// HasFirstUser returns whether the first user has been created.
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/first", nil)
if err != nil {
return false, err
}
defer res.Body.Close()
if res.StatusCode == http.StatusNotFound {
// ensure we are talking to coder and not
// some other service that returns 404
v := res.Header.Get(BuildVersionHeader)
if v == "" {
return false, xerrors.Errorf("missing build version header, not a coder instance")
}
return false, nil
}
if res.StatusCode != http.StatusOK {
return false, ReadBodyAsError(res)
}
return true, nil
}
// CreateFirstUser attempts to create the first user on a Coder deployment.
// This initial user has superadmin privileges. If >0 users exist, this request will fail.
func (c *Client) CreateFirstUser(ctx context.Context, req CreateFirstUserRequest) (CreateFirstUserResponse, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/first", req)
if err != nil {
return CreateFirstUserResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return CreateFirstUserResponse{}, ReadBodyAsError(res)
}
var resp CreateFirstUserResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// CreateUser
// Deprecated: Use CreateUserWithOrgs instead. This will be removed.
// TODO: When removing, we should rename CreateUserWithOrgs -> CreateUser
// with an alias of CreateUserWithOrgs.
func (c *Client) CreateUser(ctx context.Context, req CreateUserRequest) (User, error) {
if req.DisableLogin {
req.UserLoginType = LoginTypeNone
}
return c.CreateUserWithOrgs(ctx, CreateUserRequestWithOrgs{
Email: req.Email,
Username: req.Username,
Name: req.Name,
Password: req.Password,
UserLoginType: req.UserLoginType,
OrganizationIDs: []uuid.UUID{req.OrganizationID},
})
}
// CreateUserWithOrgs creates a new user.
func (c *Client) CreateUserWithOrgs(ctx context.Context, req CreateUserRequestWithOrgs) (User, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users", req)
if err != nil {
return User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return User{}, ReadBodyAsError(res)
}
var user User
return user, json.NewDecoder(res.Body).Decode(&user)
}
// DeleteUser deletes a user.
func (c *Client) DeleteUser(ctx context.Context, id uuid.UUID) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/users/%s", id), nil)
if err != nil {
return err
}
defer res.Body.Close()
// Check for a 200 or a 204 response. 2.14.0 accidentally included a 204 response,
// which was a breaking change, and reverted in 2.14.1.
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// UpdateUserProfile updates the username of a user.
func (c *Client) UpdateUserProfile(ctx context.Context, user string, req UpdateUserProfileRequest) (User, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/profile", user), req)
if err != nil {
return User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return User{}, ReadBodyAsError(res)
}
var resp User
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// ValidateUserPassword validates the complexity of a user password and that it is secured enough.
func (c *Client) ValidateUserPassword(ctx context.Context, req ValidateUserPasswordRequest) (ValidateUserPasswordResponse, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/validate-password", req)
if err != nil {
return ValidateUserPasswordResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return ValidateUserPasswordResponse{}, ReadBodyAsError(res)
}
var resp ValidateUserPasswordResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateUserStatus sets the user status to the given status
func (c *Client) UpdateUserStatus(ctx context.Context, user string, status UserStatus) (User, error) {
path := fmt.Sprintf("/api/v2/users/%s/status/", user)
switch status {
case UserStatusActive:
path += "activate"
case UserStatusSuspended:
path += "suspend"
default:
return User{}, xerrors.Errorf("status %q is not supported", status)
}
res, err := c.Request(ctx, http.MethodPut, path, nil)
if err != nil {
return User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return User{}, ReadBodyAsError(res)
}
var resp User
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// GetUserAppearanceSettings fetches the appearance settings for a user.
func (c *Client) GetUserAppearanceSettings(ctx context.Context, user string) (UserAppearanceSettings, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/appearance", user), nil)
if err != nil {
return UserAppearanceSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserAppearanceSettings{}, ReadBodyAsError(res)
}
var resp UserAppearanceSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateUserAppearanceSettings updates the appearance settings for a user.
func (c *Client) UpdateUserAppearanceSettings(ctx context.Context, user string, req UpdateUserAppearanceSettingsRequest) (UserAppearanceSettings, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/appearance", user), req)
if err != nil {
return UserAppearanceSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserAppearanceSettings{}, ReadBodyAsError(res)
}
var resp UserAppearanceSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// GetUserPreferenceSettings fetches the preference settings for a user.
func (c *Client) GetUserPreferenceSettings(ctx context.Context, user string) (UserPreferenceSettings, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/preferences", user), nil)
if err != nil {
return UserPreferenceSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserPreferenceSettings{}, ReadBodyAsError(res)
}
var resp UserPreferenceSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateUserPreferenceSettings updates the preference settings for a user.
func (c *Client) UpdateUserPreferenceSettings(ctx context.Context, user string, req UpdateUserPreferenceSettingsRequest) (UserPreferenceSettings, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/preferences", user), req)
if err != nil {
return UserPreferenceSettings{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserPreferenceSettings{}, ReadBodyAsError(res)
}
var resp UserPreferenceSettings
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateUserPassword updates a user password.
// It calls PUT /users/{user}/password
func (c *Client) UpdateUserPassword(ctx context.Context, user string, req UpdateUserPasswordRequest) error {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/password", user), req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// PostOrganizationMember adds a user to an organization
func (c *Client) PostOrganizationMember(ctx context.Context, organizationID uuid.UUID, user string) (OrganizationMember, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationID, user), nil)
if err != nil {
return OrganizationMember{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OrganizationMember{}, ReadBodyAsError(res)
}
var member OrganizationMember
return member, json.NewDecoder(res.Body).Decode(&member)
}
// DeleteOrganizationMember removes a user from an organization
func (c *Client) DeleteOrganizationMember(ctx context.Context, organizationID uuid.UUID, user string) error {
res, err := c.Request(ctx, http.MethodDelete, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationID, user), nil)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
type OrganizationMembersQuery struct {
UserID uuid.UUID
IncludeSystem bool
GithubUserID int64
}
func (omq OrganizationMembersQuery) AsRequestOption() RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
var sb strings.Builder
if omq.UserID != uuid.Nil {
_, _ = sb.WriteString("user_id:")
_, _ = sb.WriteString(omq.UserID.String())
_, _ = sb.WriteString(" ")
}
if omq.IncludeSystem {
_, _ = sb.WriteString("include_system:true")
}
if omq.GithubUserID != 0 {
_, _ = sb.WriteString("github_user_id:")
_, _ = sb.WriteString(strconv.FormatInt(omq.GithubUserID, 10))
_, _ = sb.WriteString(" ")
}
qs := strings.TrimSpace(sb.String())
if len(qs) == 0 {
return
}
q.Set("q", qs)
r.URL.RawQuery = q.Encode()
}
}
type OrganizationMembersQueryOption func(*OrganizationMembersQuery)
func OrganizationMembersQueryOptionUserID(userID uuid.UUID) OrganizationMembersQueryOption {
return func(query *OrganizationMembersQuery) {
query.UserID = userID
}
}
func OrganizationMembersQueryOptionIncludeSystem() OrganizationMembersQueryOption {
return func(query *OrganizationMembersQuery) {
query.IncludeSystem = true
}
}
func OrganizationMembersQueryOptionGithubUserID(githubUserID int64) OrganizationMembersQueryOption {
return func(query *OrganizationMembersQuery) {
query.GithubUserID = githubUserID
}
}
func (c *Client) OrganizationMember(ctx context.Context, organizationIdent, userIdent string) (OrganizationMemberWithUserData, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/%s", organizationIdent, userIdent), nil)
if err != nil {
return OrganizationMemberWithUserData{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OrganizationMemberWithUserData{}, ReadBodyAsError(res)
}
var member OrganizationMemberWithUserData
return member, json.NewDecoder(res.Body).Decode(&member)
}
// OrganizationMembers lists all members in an organization
func (c *Client) OrganizationMembers(ctx context.Context, organizationID uuid.UUID, opts ...OrganizationMembersQueryOption) ([]OrganizationMemberWithUserData, error) {
var query OrganizationMembersQuery
for _, opt := range opts {
opt(&query)
}
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/organizations/%s/members/", organizationID), nil, query.AsRequestOption())
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, ReadBodyAsError(res)
}
var members []OrganizationMemberWithUserData
return members, json.NewDecoder(res.Body).Decode(&members)
}
// OrganizationMembers lists filtered and paginated members in an organization
func (c *Client) OrganizationMembersPaginated(ctx context.Context, organizationID uuid.UUID, req UsersRequest) (PaginatedMembersResponse, error) {
res, err := c.Request(ctx, http.MethodGet,
fmt.Sprintf("/api/v2/organizations/%s/paginated-members", organizationID),
nil,
req.Pagination.asRequestOption(),
req.asRequestOption(),
)
if err != nil {
return PaginatedMembersResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return PaginatedMembersResponse{}, ReadBodyAsError(res)
}
var membersRes PaginatedMembersResponse
return membersRes, json.NewDecoder(res.Body).Decode(&membersRes)
}
// UpdateUserRoles grants the userID the specified roles.
// Include ALL roles the user has.
func (c *Client) UpdateUserRoles(ctx context.Context, user string, req UpdateRoles) (User, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/roles", user), req)
if err != nil {
return User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return User{}, ReadBodyAsError(res)
}
var resp User
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateOrganizationMemberRoles grants the userID the specified roles in an org.
// Include ALL roles the user has.
func (c *Client) UpdateOrganizationMemberRoles(ctx context.Context, organizationID uuid.UUID, user string, req UpdateRoles) (OrganizationMember, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/organizations/%s/members/%s/roles", organizationID, user), req)
if err != nil {
return OrganizationMember{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OrganizationMember{}, ReadBodyAsError(res)
}
var member OrganizationMember
return member, json.NewDecoder(res.Body).Decode(&member)
}
// UserRoles returns all roles the user has
func (c *Client) UserRoles(ctx context.Context, user string) (UserRoles, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/roles", user), nil)
if err != nil {
return UserRoles{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserRoles{}, ReadBodyAsError(res)
}
var roles UserRoles
return roles, json.NewDecoder(res.Body).Decode(&roles)
}
// UserOIDCClaims returns the merged OIDC claims for the authenticated user.
func (c *Client) UserOIDCClaims(ctx context.Context) (OIDCClaimsResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/oidc-claims", nil)
if err != nil {
return OIDCClaimsResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return OIDCClaimsResponse{}, ReadBodyAsError(res)
}
var resp OIDCClaimsResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// LoginWithPassword creates a session token authenticating with an email and password.
// Call `SetSessionToken()` to apply the newly acquired token to the client.
func (c *Client) LoginWithPassword(ctx context.Context, req LoginWithPasswordRequest) (LoginWithPasswordResponse, error) {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/login", req)
if err != nil {
return LoginWithPasswordResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return LoginWithPasswordResponse{}, ReadBodyAsError(res)
}
var resp LoginWithPasswordResponse
err = json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return LoginWithPasswordResponse{}, err
}
return resp, nil
}
func (c *Client) RequestOneTimePasscode(ctx context.Context, req RequestOneTimePasscodeRequest) error {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/otp/request", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
func (c *Client) ChangePasswordWithOneTimePasscode(ctx context.Context, req ChangePasswordWithOneTimePasscodeRequest) error {
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/otp/change-password", req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
return ReadBodyAsError(res)
}
return nil
}
// ConvertLoginType will send a request to convert the user from password
// based authentication to oauth based. The response has the oauth state code
// to use in the oauth flow.
func (c *Client) ConvertLoginType(ctx context.Context, req ConvertLoginRequest) (OAuthConversionResponse, error) {
return c.ConvertUserLoginType(ctx, Me, req)
}
// ConvertUserLoginType will send a request to convert the user from password
// based authentication to oauth based. The response has the oauth state code
// to use in the oauth flow.
func (c *Client) ConvertUserLoginType(ctx context.Context, user string, req ConvertLoginRequest) (OAuthConversionResponse, error) {
res, err := c.Request(ctx, http.MethodPost, fmt.Sprintf("/api/v2/users/%s/convert-login", user), req)
if err != nil {
return OAuthConversionResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusCreated {
return OAuthConversionResponse{}, ReadBodyAsError(res)
}
var resp OAuthConversionResponse
err = json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return OAuthConversionResponse{}, err
}
return resp, nil
}
// Logout calls the /logout API
// Call `ClearSessionToken()` to clear the session token of the client.
func (c *Client) Logout(ctx context.Context) error {
// Since `LoginWithPassword` doesn't actually set a SessionToken
// (it requires a call to SetSessionToken), this is essentially a no-op
res, err := c.Request(ctx, http.MethodPost, "/api/v2/users/logout", nil)
if err != nil {
return err
}
defer res.Body.Close()
return nil
}
// User returns a user for the ID/username provided.
func (c *Client) User(ctx context.Context, userIdent string) (User, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s", userIdent), nil)
if err != nil {
return User{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return User{}, ReadBodyAsError(res)
}
var user User
return user, json.NewDecoder(res.Body).Decode(&user)
}
// UserQuietHoursSchedule returns the quiet hours settings for the user. This
// endpoint only exists in enterprise editions.
func (c *Client) UserQuietHoursSchedule(ctx context.Context, userIdent string) (UserQuietHoursScheduleResponse, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/quiet-hours", userIdent), nil)
if err != nil {
return UserQuietHoursScheduleResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res)
}
var resp UserQuietHoursScheduleResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// UpdateUserQuietHoursSchedule updates the quiet hours settings for the user.
// This endpoint only exists in enterprise editions.
func (c *Client) UpdateUserQuietHoursSchedule(ctx context.Context, userIdent string, req UpdateUserQuietHoursScheduleRequest) (UserQuietHoursScheduleResponse, error) {
res, err := c.Request(ctx, http.MethodPut, fmt.Sprintf("/api/v2/users/%s/quiet-hours", userIdent), req)
if err != nil {
return UserQuietHoursScheduleResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return UserQuietHoursScheduleResponse{}, ReadBodyAsError(res)
}
var resp UserQuietHoursScheduleResponse
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
// Users returns all users according to the request parameters. If no parameters are set,
// the default behavior is to return all users in a single page.
func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, error) {
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users", nil,
req.Pagination.asRequestOption(),
req.asRequestOption(),
)
if err != nil {
return GetUsersResponse{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return GetUsersResponse{}, ReadBodyAsError(res)
}
var usersRes GetUsersResponse
return usersRes, json.NewDecoder(res.Body).Decode(&usersRes)
}
// OrganizationsByUser returns all organizations the user is a member of.
func (c *Client) OrganizationsByUser(ctx context.Context, user string) ([]Organization, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations", user), nil)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode > http.StatusOK {
return nil, ReadBodyAsError(res)
}
var orgs []Organization
return orgs, json.NewDecoder(res.Body).Decode(&orgs)
}
func (c *Client) OrganizationByUserAndName(ctx context.Context, user string, name string) (Organization, error) {
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/organizations/%s", user, name), nil)
if err != nil {
return Organization{}, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return Organization{}, ReadBodyAsError(res)
}
var org Organization
return org, json.NewDecoder(res.Body).Decode(&org)
}