mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 07:19:00 +08:00
feat: bulk invite user api (#6057)
This commit is contained in:
parent
96cb8053df
commit
fc4b55cb34
@ -486,6 +486,7 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
|
||||
|
||||
// === Authentication APIs ===
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/bulk", am.AdminAccess(aH.inviteUsers)).Methods(http.MethodPost)
|
||||
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
|
||||
router.HandleFunc("/api/v1/invite/{email}", am.AdminAccess(aH.revokeInvite)).Methods(http.MethodDelete)
|
||||
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.listPendingInvites)).Methods(http.MethodGet)
|
||||
@ -1976,6 +1977,32 @@ func (aH *APIHandler) inviteUser(w http.ResponseWriter, r *http.Request) {
|
||||
aH.WriteJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func (aH *APIHandler) inviteUsers(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := parseInviteUsersRequest(r)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := auth.InviteUsers(ctx, req)
|
||||
if err != nil {
|
||||
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||
return
|
||||
}
|
||||
// Check the response status and set the appropriate HTTP status code
|
||||
if response.Status == "failure" {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else if response.Status == "partial_success" {
|
||||
w.WriteHeader(http.StatusPartialContent) // 206 Partial Content
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK) // 200 OK for success
|
||||
}
|
||||
|
||||
aH.WriteJSON(w, r, response)
|
||||
}
|
||||
|
||||
// getInvite returns the invite object details for the given invite token. We do not need to
|
||||
// protect this API because invite token itself is meant to be private.
|
||||
func (aH *APIHandler) getInvite(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
|
||||
"go.signoz.io/signoz/pkg/query-service/auth"
|
||||
"go.signoz.io/signoz/pkg/query-service/common"
|
||||
"go.signoz.io/signoz/pkg/query-service/constants"
|
||||
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||||
"go.signoz.io/signoz/pkg/query-service/model"
|
||||
v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
|
||||
@ -724,6 +725,45 @@ func parseInviteRequest(r *http.Request) (*model.InviteRequest, error) {
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func isValidRole(role string) bool {
|
||||
switch role {
|
||||
case constants.AdminGroup, constants.EditorGroup, constants.ViewerGroup:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseInviteUsersRequest(r *http.Request) (*model.BulkInviteRequest, error) {
|
||||
var req model.BulkInviteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Validate that the request contains users
|
||||
if len(req.Users) == 0 {
|
||||
return nil, fmt.Errorf("no users provided for invitation")
|
||||
}
|
||||
|
||||
// Trim spaces and validate each user
|
||||
for i := range req.Users {
|
||||
req.Users[i].Email = strings.TrimSpace(req.Users[i].Email)
|
||||
if req.Users[i].Email == "" {
|
||||
return nil, fmt.Errorf("email is required for each user")
|
||||
}
|
||||
if req.Users[i].Name == "" {
|
||||
return nil, fmt.Errorf("name is required for each user")
|
||||
}
|
||||
if req.Users[i].FrontendBaseUrl == "" {
|
||||
return nil, fmt.Errorf("frontendBaseUrl is required for each user")
|
||||
}
|
||||
if !isValidRole(req.Users[i].Role) {
|
||||
return nil, fmt.Errorf("invalid role for user: %s", req.Users[i].Email)
|
||||
}
|
||||
}
|
||||
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
func parseSetApdexScoreRequest(r *http.Request) (*model.ApdexSettings, error) {
|
||||
var req model.ApdexSettings
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
|
@ -61,6 +61,16 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons
|
||||
return nil, errors.New("User already exists with the same email")
|
||||
}
|
||||
|
||||
// Check if an invite already exists
|
||||
invite, apiErr := dao.DB().GetInviteFromEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check existing invite")
|
||||
}
|
||||
|
||||
if invite != nil {
|
||||
return nil, errors.New("An invite already exists for this email")
|
||||
}
|
||||
|
||||
if err := validateInviteRequest(req); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid invite request")
|
||||
}
|
||||
@ -79,6 +89,113 @@ func Invite(ctx context.Context, req *model.InviteRequest) (*model.InviteRespons
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(err, "failed to query admin user from the DB")
|
||||
}
|
||||
|
||||
inv := &model.InvitationObject{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Token: token,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Role: req.Role,
|
||||
OrgId: au.OrgId,
|
||||
}
|
||||
|
||||
if err := dao.DB().CreateInviteEntry(ctx, inv); err != nil {
|
||||
return nil, errors.Wrap(err.Err, "failed to write to DB")
|
||||
}
|
||||
|
||||
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_EVENT_USER_INVITATION_SENT, map[string]interface{}{
|
||||
"invited user email": req.Email,
|
||||
}, au.Email, true, false)
|
||||
|
||||
// send email if SMTP is enabled
|
||||
if os.Getenv("SMTP_ENABLED") == "true" && req.FrontendBaseUrl != "" {
|
||||
inviteEmail(req, au, token)
|
||||
}
|
||||
|
||||
return &model.InviteResponse{Email: inv.Email, InviteToken: inv.Token}, nil
|
||||
}
|
||||
|
||||
func InviteUsers(ctx context.Context, req *model.BulkInviteRequest) (*model.BulkInviteResponse, error) {
|
||||
response := &model.BulkInviteResponse{
|
||||
Status: "success",
|
||||
Summary: model.InviteSummary{TotalInvites: len(req.Users)},
|
||||
SuccessfulInvites: []model.SuccessfulInvite{},
|
||||
FailedInvites: []model.FailedInvite{},
|
||||
}
|
||||
|
||||
jwtAdmin, ok := ExtractJwtFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to extract admin jwt token")
|
||||
}
|
||||
|
||||
adminUser, err := validateUser(jwtAdmin)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to validate admin jwt token")
|
||||
}
|
||||
|
||||
au, apiErr := dao.DB().GetUser(ctx, adminUser.Id)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "failed to query admin user from the DB")
|
||||
}
|
||||
|
||||
for _, inviteReq := range req.Users {
|
||||
inviteResp, err := inviteUser(ctx, &inviteReq, au)
|
||||
if err != nil {
|
||||
response.FailedInvites = append(response.FailedInvites, model.FailedInvite{
|
||||
Email: inviteReq.Email,
|
||||
Error: err.Error(),
|
||||
})
|
||||
response.Summary.FailedInvites++
|
||||
} else {
|
||||
response.SuccessfulInvites = append(response.SuccessfulInvites, model.SuccessfulInvite{
|
||||
Email: inviteResp.Email,
|
||||
InviteLink: fmt.Sprintf("%s/signup?token=%s", inviteReq.FrontendBaseUrl, inviteResp.InviteToken),
|
||||
Status: "sent",
|
||||
})
|
||||
response.Summary.SuccessfulInvites++
|
||||
}
|
||||
}
|
||||
|
||||
// Update the status based on the results
|
||||
if response.Summary.FailedInvites == response.Summary.TotalInvites {
|
||||
response.Status = "failure"
|
||||
} else if response.Summary.FailedInvites > 0 {
|
||||
response.Status = "partial_success"
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Helper function to handle individual invites
|
||||
func inviteUser(ctx context.Context, req *model.InviteRequest, au *model.UserPayload) (*model.InviteResponse, error) {
|
||||
token, err := utils.RandomHex(opaqueTokenSize)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate invite token")
|
||||
}
|
||||
|
||||
user, apiErr := dao.DB().GetUserByEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check already existing user")
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
return nil, errors.New("User already exists with the same email")
|
||||
}
|
||||
|
||||
// Check if an invite already exists
|
||||
invite, apiErr := dao.DB().GetInviteFromEmail(ctx, req.Email)
|
||||
if apiErr != nil {
|
||||
return nil, errors.Wrap(apiErr.Err, "Failed to check existing invite")
|
||||
}
|
||||
|
||||
if invite != nil {
|
||||
return nil, errors.New("An invite already exists for this email")
|
||||
}
|
||||
|
||||
if err := validateInviteRequest(req); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid invite request")
|
||||
}
|
||||
|
||||
inv := &model.InvitationObject{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
|
@ -27,6 +27,34 @@ type InvitationResponseObject struct {
|
||||
Organization string `json:"organization" db:"organization"`
|
||||
}
|
||||
|
||||
type BulkInviteRequest struct {
|
||||
Users []InviteRequest `json:"users"`
|
||||
}
|
||||
|
||||
type BulkInviteResponse struct {
|
||||
Status string `json:"status"`
|
||||
Summary InviteSummary `json:"summary"`
|
||||
SuccessfulInvites []SuccessfulInvite `json:"successful_invites"`
|
||||
FailedInvites []FailedInvite `json:"failed_invites"`
|
||||
}
|
||||
|
||||
type InviteSummary struct {
|
||||
TotalInvites int `json:"total_invites"`
|
||||
SuccessfulInvites int `json:"successful_invites"`
|
||||
FailedInvites int `json:"failed_invites"`
|
||||
}
|
||||
|
||||
type SuccessfulInvite struct {
|
||||
Email string `json:"email"`
|
||||
InviteLink string `json:"invite_link"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type FailedInvite struct {
|
||||
Email string `json:"email"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
|
Loading…
x
Reference in New Issue
Block a user