mirror of
https://github.com/SigNoz/signoz.git
synced 2026-03-09 23:12:20 +00:00
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* feat: deprecate user invite table * fix: handle soft deleted users flow * fix: handle edge cases for authentication and reset password flow * feat: integration tests with fixes for new flow * fix: array for grants * fix: edge cases for reset token and context api * chore: remove all code related to old invite flow * fix: openapi specs * fix: integration tests and minor naming change * fix: integration tests fmtlint * feat: improve invitation email template * fix: role tests * fix: context api * fix: openapi frontend * chore: rename countbyorgid to activecountbyorgid * fix: a deleted user cannot recycled, creating a new one * feat: migrate existing invites to user as pending invite status * fix: error from GetUsersByEmailAndOrgID * feat: add backward compatibility to existing apis using new invite flow * chore: change ordering of apis in server * chore: change ordering of apis in server * fix: filter active users in role and org id check * fix: check deleted user in reset password flow * chore: address some review comments, add back countbyorgid method * chore: move to bulk inserts for migrating existing invites * fix: wrap funcs to transactions, and fix openapi specs * fix: move reset link method to types, also move authz grants outside transation * fix: transaction issues * feat: helper method ErrIfDeleted for user * fix: error code for errifdeleted in user * fix: soft delete store method * fix: password authn tests also add old invite flow test * fix: callbackauthn tests * fix: remove extra oidc tests * fix: callback authn tests oidc * chore: address review comments and optimise bulk invite api * fix: use db ctx in various places * fix: fix duplicate email invite issue and add partial invite * fix: openapi specs * fix: errifpending * fix: user status persistence * fix: edge cases * chore: add tests for partial index too * feat: use composite unique index on users table instead of partial one * chore: move duplicate email check to unmarshaljson and query user again in accept invite * fix: make 068 migratin idempotent * chore: remove unused emails var * chore: add a temp filter to show only active users in frontend until next frontend fix * chore: remove one check from register flow testing until temp code is removed * chore: remove commented code from tests * chore: address frontend review comments * chore: address frontend review comments
521 lines
13 KiB
Go
521 lines
13 KiB
Go
package impluser
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/SigNoz/signoz/pkg/errors"
|
|
"github.com/SigNoz/signoz/pkg/http/binding"
|
|
"github.com/SigNoz/signoz/pkg/http/render"
|
|
root "github.com/SigNoz/signoz/pkg/modules/user"
|
|
"github.com/SigNoz/signoz/pkg/types"
|
|
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
|
"github.com/SigNoz/signoz/pkg/types/integrationtypes"
|
|
"github.com/SigNoz/signoz/pkg/valuer"
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
type handler struct {
|
|
module root.Module
|
|
getter root.Getter
|
|
}
|
|
|
|
func NewHandler(module root.Module, getter root.Getter) root.Handler {
|
|
return &handler{module: module, getter: getter}
|
|
}
|
|
|
|
func (h *handler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req := new(types.PostableAcceptInvite)
|
|
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
user, err := h.module.AcceptInvite(ctx, req.InviteToken, req.Password)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusCreated, user)
|
|
}
|
|
|
|
func (h *handler) CreateInvite(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
var req types.PostableInvite
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
invites, err := h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &types.PostableBulkInviteRequest{
|
|
Invites: []types.PostableInvite{req},
|
|
})
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusCreated, invites[0])
|
|
}
|
|
|
|
func (h *handler) CreateBulkInvite(rw http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
var req types.PostableBulkInviteRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
// Validate that the request contains users
|
|
if len(req.Invites) == 0 {
|
|
render.Error(rw, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no invites provided for invitation"))
|
|
return
|
|
}
|
|
|
|
_, err = h.module.CreateBulkInvite(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID), &req)
|
|
if err != nil {
|
|
render.Error(rw, err)
|
|
return
|
|
}
|
|
|
|
render.Success(rw, http.StatusCreated, nil)
|
|
}
|
|
|
|
func (h *handler) GetInvite(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
token := mux.Vars(r)["token"]
|
|
invite, err := h.module.GetInviteByToken(ctx, token)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, invite)
|
|
}
|
|
|
|
func (h *handler) ListInvite(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
invites, err := h.module.ListInvite(ctx, claims.OrgID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, invites)
|
|
}
|
|
|
|
func (h *handler) DeleteInvite(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (h *handler) GetUser(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (h *handler) GetMyUser(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
user, err := h.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(claims.UserID))
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, user)
|
|
}
|
|
|
|
func (h *handler) ListUsers(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
users, err := h.getter.ListByOrgID(ctx, valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// temp code - show only active users
|
|
users = slices.DeleteFunc(users, func(user *types.User) bool { return user.Status != types.UserStatusActive })
|
|
|
|
render.Success(w, http.StatusOK, users)
|
|
}
|
|
|
|
func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
var user types.User
|
|
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
updatedUser, err := h.module.UpdateUser(ctx, valuer.MustNewUUID(claims.OrgID), id, &user, claims.UserID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, updatedUser)
|
|
}
|
|
|
|
func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
if err := h.module.DeleteUser(ctx, valuer.MustNewUUID(claims.OrgID), id, claims.UserID); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (handler *handler) GetResetPasswordToken(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
user, err := handler.getter.GetByOrgIDAndID(ctx, valuer.MustNewUUID(claims.OrgID), valuer.MustNewUUID(id))
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
token, err := handler.module.GetOrCreateResetPasswordToken(ctx, user.ID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, token)
|
|
}
|
|
|
|
func (handler *handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req := new(types.PostableResetPassword)
|
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
err := handler.module.UpdatePasswordByResetPasswordToken(ctx, req.Token, req.Password)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (handler *handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
var req types.ChangePasswordRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
err := handler.module.UpdatePassword(ctx, req.UserID, req.OldPassword, req.NewPassword)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (h *handler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
req := new(types.PostableForgotPassword)
|
|
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
err := h.module.ForgotPassword(ctx, req.OrgID, req.Email, req.FrontendBaseURL)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (h *handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
req := new(types.PostableAPIKey)
|
|
if err := json.NewDecoder(r.Body).Decode(req); err != nil {
|
|
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
|
|
return
|
|
}
|
|
|
|
apiKey, err := types.NewStorableAPIKey(
|
|
req.Name,
|
|
valuer.MustNewUUID(claims.UserID),
|
|
req.Role,
|
|
req.ExpiresInDays,
|
|
)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
err = h.module.CreateAPIKey(ctx, apiKey)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
createdApiKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), apiKey.ID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// just corrected the status code, response is same,
|
|
render.Success(w, http.StatusCreated, createdApiKey)
|
|
}
|
|
|
|
func (h *handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
apiKeys, err := h.module.ListAPIKeys(ctx, valuer.MustNewUUID(claims.OrgID))
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// for backward compatibility
|
|
if len(apiKeys) == 0 {
|
|
render.Success(w, http.StatusOK, []types.GettableAPIKey{})
|
|
return
|
|
}
|
|
|
|
result := make([]*types.GettableAPIKey, len(apiKeys))
|
|
for i, apiKey := range apiKeys {
|
|
result[i] = types.NewGettableAPIKeyFromStorableAPIKey(apiKey)
|
|
}
|
|
|
|
render.Success(w, http.StatusOK, result)
|
|
|
|
}
|
|
|
|
func (h *handler) UpdateAPIKey(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
req := types.StorableAPIKey{}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
render.Error(w, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to decode api key"))
|
|
return
|
|
}
|
|
|
|
idStr := mux.Vars(r)["id"]
|
|
id, err := valuer.NewUUID(idStr)
|
|
if err != nil {
|
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
|
return
|
|
}
|
|
|
|
//get the API Key
|
|
existingAPIKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// get the user
|
|
createdByUser, err := h.getter.Get(ctx, existingAPIKey.UserID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
|
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
|
return
|
|
}
|
|
|
|
err = h.module.UpdateAPIKey(ctx, id, &req, valuer.MustNewUUID(claims.UserID))
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (h *handler) RevokeAPIKey(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
claims, err := authtypes.ClaimsFromContext(ctx)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
idStr := mux.Vars(r)["id"]
|
|
id, err := valuer.NewUUID(idStr)
|
|
if err != nil {
|
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "id is not a valid uuid-v7"))
|
|
return
|
|
}
|
|
|
|
//get the API Key
|
|
existingAPIKey, err := h.module.GetAPIKey(ctx, valuer.MustNewUUID(claims.OrgID), id)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
// get the user
|
|
createdByUser, err := h.getter.Get(ctx, existingAPIKey.UserID)
|
|
if err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
if slices.Contains(integrationtypes.AllIntegrationUserEmails, integrationtypes.IntegrationUserEmail(createdByUser.Email.String())) {
|
|
render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "API Keys for integration users cannot be revoked"))
|
|
return
|
|
}
|
|
|
|
if err := h.module.RevokeAPIKey(ctx, id, valuer.MustNewUUID(claims.UserID)); err != nil {
|
|
render.Error(w, err)
|
|
return
|
|
}
|
|
|
|
render.Success(w, http.StatusNoContent, nil)
|
|
}
|