feat: redesign project sharing & initial registration
redesigned the project sharing popup to be a multi select dropdown that populates the options by using the input as a fuzzy search filter on the current users & invited users. users can now also be directly invited by email from the project share window. if invited this way, then the user will receive an email that sends them to a registration page, then a confirmation page. the initial registration was always redone so that it uses a similar system to the above in that it now will accept the first registered user if there are no other accounts (besides 'system').
This commit is contained in:
@ -31,15 +31,33 @@ type NewUserAccount struct {
|
||||
Email string
|
||||
}
|
||||
|
||||
// RegisterUserRequestData is the request data for registering a new user (duh)
|
||||
type RegisterUserRequestData struct {
|
||||
User NewUserAccount
|
||||
}
|
||||
|
||||
type RegisteredUserResponseData struct {
|
||||
Setup bool `json:"setup"`
|
||||
}
|
||||
|
||||
// ConfirmUserRequestData is the request data for upgrading an invited user to a normal user
|
||||
type ConfirmUserRequestData struct {
|
||||
ConfirmToken string
|
||||
}
|
||||
|
||||
// InstallRequestData is the request data for installing new Taskcafe app
|
||||
type InstallRequestData struct {
|
||||
User NewUserAccount
|
||||
}
|
||||
|
||||
type Setup struct {
|
||||
ConfirmToken string `json:"confirmToken"`
|
||||
}
|
||||
|
||||
// LoginResponseData is the response data for when a user logs in
|
||||
type LoginResponseData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
IsInstalled bool `json:"isInstalled"`
|
||||
Setup bool `json:"setup"`
|
||||
}
|
||||
|
||||
// LogoutResponseData is the response data for when a user logs out
|
||||
@ -60,30 +78,24 @@ type AvatarUploadResponseData struct {
|
||||
|
||||
// RefreshTokenHandler handles when a user attempts to refresh token
|
||||
func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := h.repo.GetSystemOptionByKey(r.Context(), "is_installed")
|
||||
if err == sql.ErrNoRows {
|
||||
user, err := h.repo.GetUserAccountByUsername(r.Context(), "system")
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.InstallOnly, user.RoleCode, h.jwtKey)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: false})
|
||||
|
||||
userExists, err := h.repo.HasAnyUser(r.Context())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.WithError(err).Error("issue while fetching if user accounts exist")
|
||||
return
|
||||
} else if err != nil {
|
||||
log.WithError(err).Error("get system option")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
log.WithField("userExists", userExists).Info("checking if setup")
|
||||
if !userExists {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: "", Setup: true})
|
||||
return
|
||||
}
|
||||
|
||||
c, err := r.Cookie("refreshToken")
|
||||
if err != nil {
|
||||
if err == http.ErrNoCookie {
|
||||
log.Warn("no cookie")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@ -112,6 +124,14 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.WithFields(log.Fields{
|
||||
"username": user.Username,
|
||||
}).Warn("attempt to refresh token with inactive user")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
refreshCreatedAt := time.Now().UTC()
|
||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{token.UserID, refreshCreatedAt, refreshExpiresAt})
|
||||
@ -119,13 +139,17 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
err = h.repo.DeleteRefreshTokenByID(r.Context(), token.TokenID)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("here 1")
|
||||
accessTokenString, err := auth.NewAccessToken(token.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("here 2")
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refreshToken",
|
||||
@ -133,7 +157,7 @@ func (h *TaskcafeHandler) RefreshTokenHandler(w http.ResponseWriter, r *http.Req
|
||||
Expires: refreshExpiresAt,
|
||||
HttpOnly: true,
|
||||
})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, IsInstalled: true})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{AccessToken: accessTokenString, Setup: false})
|
||||
}
|
||||
|
||||
// LogoutHandler removes all refresh tokens to log out user
|
||||
@ -175,6 +199,14 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !user.Active {
|
||||
log.WithFields(log.Fields{
|
||||
"username": requestData.Username,
|
||||
}).Warn("attempt to login with inactive user")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(requestData.Password))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
@ -203,6 +235,7 @@ func (h *TaskcafeHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
// TODO: remove
|
||||
// InstallHandler creates first user on fresh install
|
||||
func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if restricted, ok := r.Context().Value("restricted_mode").(auth.RestrictedMode); ok {
|
||||
@ -266,6 +299,172 @@ func (h *TaskcafeHandler) InstallHandler(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
func (h *TaskcafeHandler) ConfirmUser(w http.ResponseWriter, r *http.Request) {
|
||||
usersExist, err := h.repo.HasActiveUser(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue checking if user accounts exist")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var user db.UserAccount
|
||||
if !usersExist {
|
||||
log.Info("setting first inactive user to active")
|
||||
user, err = h.repo.SetFirstUserActive(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue checking if user accounts exist")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var requestData ConfirmUserRequestData
|
||||
err = json.NewDecoder(r.Body).Decode(&requestData)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue decoding request data")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
confirmTokenID, err := uuid.Parse(requestData.ConfirmToken)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue parsing confirm token")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
confirmToken, err := h.repo.GetConfirmTokenByID(r.Context(), confirmTokenID)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue getting token by id")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err = h.repo.SetUserActiveByEmail(r.Context(), confirmToken.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue getting account by email")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
projects, err := h.repo.GetProjectsForInvitedMember(r.Context(), user.Email)
|
||||
for _, project := range projects {
|
||||
member, err := h.repo.CreateProjectMember(r.Context(),
|
||||
db.CreateProjectMemberParams{
|
||||
ProjectID: project,
|
||||
UserID: user.UserID,
|
||||
AddedAt: now,
|
||||
RoleCode: "member",
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue creating project member")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.WithField("memberID", member.ProjectMemberID).Info("creating project member")
|
||||
err = h.repo.DeleteProjectMemberInvitedForEmail(r.Context(), user.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue deleting project member invited")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = h.repo.DeleteUserAccountInvitedForEmail(r.Context(), user.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue deleting user account invited")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = h.repo.DeleteConfirmTokenForEmail(r.Context(), user.Email)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue deleting confirm token")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
refreshCreatedAt := time.Now().UTC()
|
||||
refreshExpiresAt := refreshCreatedAt.AddDate(0, 0, 1)
|
||||
refreshTokenString, err := h.repo.CreateRefreshToken(r.Context(), db.CreateRefreshTokenParams{user.UserID, refreshCreatedAt, refreshExpiresAt})
|
||||
|
||||
accessTokenString, err := auth.NewAccessToken(user.UserID.String(), auth.Unrestricted, user.RoleCode, h.jwtKey)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "refreshToken",
|
||||
Value: refreshTokenString.TokenID.String(),
|
||||
Expires: refreshExpiresAt,
|
||||
HttpOnly: true,
|
||||
})
|
||||
json.NewEncoder(w).Encode(LoginResponseData{accessTokenString, false})
|
||||
}
|
||||
|
||||
func (h *TaskcafeHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
|
||||
userExists, err := h.repo.HasAnyUser(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue checking if user accounts exist")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var requestData RegisterUserRequestData
|
||||
err = json.NewDecoder(r.Body).Decode(&requestData)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("issue decoding register user request data")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if userExists {
|
||||
_, err := h.repo.GetInvitedUserByEmail(r.Context(), requestData.User.Email)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
hasActiveUser, err := h.repo.HasActiveUser(r.Context())
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error checking for active user")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
if !hasActiveUser {
|
||||
json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: true})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
log.WithError(err).Error("error while retrieving invited user by email")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: accept user if public registration is enabled
|
||||
|
||||
createdAt := time.Now().UTC()
|
||||
hashedPwd, err := bcrypt.GenerateFromPassword([]byte(requestData.User.Password), 14)
|
||||
if err != nil {
|
||||
log.Error("issue generating passoed")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
user, err := h.repo.CreateUserAccount(r.Context(), db.CreateUserAccountParams{
|
||||
FullName: requestData.User.FullName,
|
||||
Username: requestData.User.Username,
|
||||
Initials: requestData.User.Initials,
|
||||
Email: requestData.User.Email,
|
||||
PasswordHash: string(hashedPwd),
|
||||
CreatedAt: createdAt,
|
||||
RoleCode: "admin",
|
||||
Active: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("issue registering user account")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.WithField("username", user.UserID).Info("registered new user account")
|
||||
json.NewEncoder(w).Encode(RegisteredUserResponseData{Setup: !userExists})
|
||||
}
|
||||
|
||||
// Routes registers all authentication routes
|
||||
func (rs authResource) Routes(taskcafeHandler TaskcafeHandler) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
@ -19,6 +19,7 @@ type AuthenticationMiddleware struct {
|
||||
// Middleware returns the middleware handler
|
||||
func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestID := uuid.New()
|
||||
bearerTokenRaw := r.Header.Get("Authorization")
|
||||
splitToken := strings.Split(bearerTokenRaw, "Bearer")
|
||||
if len(splitToken) != 2 {
|
||||
@ -61,6 +62,7 @@ func (m *AuthenticationMiddleware) Middleware(next http.Handler) http.Handler {
|
||||
ctx := context.WithValue(r.Context(), utils.UserIDKey, userID)
|
||||
ctx = context.WithValue(ctx, utils.RestrictedModeKey, accessClaims.Restricted)
|
||||
ctx = context.WithValue(ctx, utils.OrgRoleKey, accessClaims.OrgRole)
|
||||
ctx = context.WithValue(ctx, utils.ReqIDKey, requestID)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
|
@ -87,13 +87,13 @@ func NewRouter(dbConnection *sqlx.DB, jwtKey []byte) (chi.Router, error) {
|
||||
mux.Mount("/auth", authResource{}.Routes(taskcafeHandler))
|
||||
mux.Handle("/__graphql", graph.NewPlaygroundHandler("/graphql"))
|
||||
mux.Mount("/uploads/", http.StripPrefix("/uploads/", imgServer))
|
||||
|
||||
mux.Post("/auth/confirm", taskcafeHandler.ConfirmUser)
|
||||
mux.Post("/auth/register", taskcafeHandler.RegisterUser)
|
||||
})
|
||||
auth := AuthenticationMiddleware{jwtKey}
|
||||
r.Group(func(mux chi.Router) {
|
||||
mux.Use(auth.Middleware)
|
||||
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
|
||||
mux.Post("/auth/install", taskcafeHandler.InstallHandler)
|
||||
mux.Handle("/graphql", graph.NewHandler(*repository))
|
||||
})
|
||||
|
||||
|
Reference in New Issue
Block a user