-
Notifications
You must be signed in to change notification settings - Fork 2
/
invites.go
154 lines (134 loc) · 5.66 KB
/
invites.go
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
package tenant
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/oklog/ulid/v2"
qd "github.com/rotationalio/ensign/pkg/quarterdeck/api/v1"
"github.com/rotationalio/ensign/pkg/quarterdeck/middleware"
"github.com/rotationalio/ensign/pkg/quarterdeck/tokens"
"github.com/rotationalio/ensign/pkg/tenant/api/v1"
"github.com/rotationalio/ensign/pkg/tenant/db"
"github.com/rotationalio/ensign/pkg/utils/responses"
"github.com/rotationalio/ensign/pkg/utils/sentry"
"github.com/rotationalio/ensign/pkg/utils/ulids"
)
// InvitePreview returns "preview" information about an invite given a token. This
// endpoint must not be authenticated because unauthorized users should be able to
// accept organization invitations. Frontends should use this endpoint to validate an
// invitation token after the user has clicked on an invitation link in their email.
// The preview must contain enough information so the user knows which organization
// they are joining and also whether or not the email address is already registered to
// an account. This allows frontends to know whether or not to prompt the user to
// login or to create a new account.
//
// Route: /invites/:token
func (s *Server) InvitePreview(c *gin.Context) {
var err error
token := c.Param("token")
// Call Quarterdeck to retrieve the invite preview.
var rep *qd.UserInvitePreview
if rep, err = s.quarterdeck.InvitePreview(c.Request.Context(), token); err != nil {
sentry.Debug(c).Err(err).Msg("tracing quarterdeck error in tenant")
api.ReplyQuarterdeckError(c, err)
return
}
// Create the preview response
out := &api.MemberInvitePreview{
Email: rep.Email,
OrgName: rep.OrgName,
InviterName: rep.InviterName,
Role: rep.Role,
HasAccount: rep.UserExists,
}
c.JSON(http.StatusOK, out)
}
// InviteAccept is an authenticated endpoint to accept an invitation to join an
// organization. The invitation token must be provided in the request body, and the
// email in the user claims must match the email address in the token. If the
// invitation is invalid this endpoint returns a 404. If successful, the user is logged
// into the organization and credentials are set as cookies. Frontends should use this
// endpoint when a user is already logged in and is accepting an invitation. If the
// user is not logged in, the Login endpoint should be used instead.
//
// Route: /invites/accept
func (s *Server) InviteAccept(c *gin.Context) {
var (
ctx context.Context
req *api.MemberInviteToken
err error
)
// User credentials are required for the Quarterdeck request
if ctx, err = middleware.ContextFromRequest(c); err != nil {
sentry.Error(c).Err(err).Msg("could not get user credentials from authenticated request")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
}
// Parse the token in the request body
if err = c.BindJSON(&req); err != nil {
sentry.Warn(c).Err(err).Msg("could not parse accept invite token request")
c.JSON(http.StatusBadRequest, api.ErrorResponse(responses.ErrTryLoginAgain))
return
}
if req.Token == "" {
sentry.Warn(c).Msg("missing token in accept invite request")
c.JSON(http.StatusBadRequest, api.ErrorResponse(responses.ErrTryLoginAgain))
return
}
// Create the Quarterdeck request with the token
acceptRequest := &qd.UserInviteToken{
Token: req.Token,
}
// Call Quarterdeck to accept the invite
var rep *qd.LoginReply
if rep, err = s.quarterdeck.InviteAccept(ctx, acceptRequest); err != nil {
sentry.Debug(c).Err(err).Msg("tracing quarterdeck error in tenant")
api.ReplyQuarterdeckError(c, err)
return
}
// Parse the new claims from the access token
var newClaims *tokens.Claims
if newClaims, err = tokens.ParseUnverifiedTokenClaims(rep.AccessToken); err != nil {
sentry.Error(c).Err(err).Msg("could not parse claims from newly issued access token")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
return
}
var orgID ulid.ULID
if orgID = newClaims.ParseOrgID(); ulids.IsZero(orgID) {
sentry.Error(c).Msg("could not parse orgID from newly issued claims")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
return
}
var userID ulid.ULID
if userID = newClaims.ParseUserID(); ulids.IsZero(userID) {
sentry.Error(c).Msg("could not parse userID from newly issued claims")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
return
}
// Retrieve member from the database to update the invitation status
var member *db.Member
if member, err = db.RetrieveMember(ctx, orgID, userID); err != nil {
sentry.Error(c).Err(err).Msg("no member record for invited user")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
return
}
// Update member record to reflect that the user has accepted the invitation
member.JoinedAt = newClaims.IssuedAt.Time
member.LastActivity = member.JoinedAt
if err = db.UpdateMember(ctx, member); err != nil {
sentry.Error(c).Err(err).Msg("could not update member record after accepting invite")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
return
}
// Set the access and refresh tokens as cookies for the front-end
if err := middleware.SetAuthCookies(c, rep.AccessToken, rep.RefreshToken, s.conf.Auth.CookieDomain); err != nil {
sentry.Error(c).Err(err).Msg("could not set access and refresh token cookies")
c.JSON(http.StatusInternalServerError, api.ErrorResponse(responses.ErrSomethingWentWrong))
return
}
out := &api.AuthReply{
AccessToken: rep.AccessToken,
RefreshToken: rep.RefreshToken,
LastLogin: rep.LastLogin,
}
c.JSON(http.StatusOK, out)
}