You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-27 00:51:13 +02:00
invite users
This commit is contained in:
@ -49,12 +49,7 @@ func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Req
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusValues []interface{}
|
statusOpts := web.NewEnumResponse(ctx, nil, project.ProjectStatus_ValuesInterface()...)
|
||||||
for _, v := range project.ProjectStatus_Values {
|
|
||||||
statusValues = append(statusValues, string(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
statusOpts := web.NewEnumResponse(ctx, nil, statusValues...)
|
|
||||||
|
|
||||||
statusFilterItems := []datatable.FilterOptionItem{}
|
statusFilterItems := []datatable.FilterOptionItem{}
|
||||||
for _, opt := range statusOpts.Options {
|
for _, opt := range statusOpts.Options {
|
||||||
|
@ -68,6 +68,10 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
|
|||||||
app.Handle("GET", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("GET", "/users/:user_id/update", us.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("POST", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("POST", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
|
app.Handle("POST", "/users/invite/:hash", us.InviteAccept)
|
||||||
|
app.Handle("GET", "/users/invite/:hash", us.InviteAccept)
|
||||||
|
app.Handle("POST", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
|
app.Handle("GET", "/users/invite", us.Invite, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
|
||||||
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
|
||||||
|
@ -230,6 +230,9 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append the query param value to the request.
|
||||||
|
req.ResetHash = params["hash"]
|
||||||
|
|
||||||
u, err := user.ResetConfirm(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
|
u, err := user.ResetConfirm(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch errors.Cause(err) {
|
switch errors.Cause(err) {
|
||||||
@ -267,8 +270,6 @@ func (h *User) ResetConfirm(ctx context.Context, w http.ResponseWriter, r *http.
|
|||||||
|
|
||||||
// Redirect the user to the dashboard.
|
// Redirect the user to the dashboard.
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
} else {
|
|
||||||
req.ResetHash = params["hash"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -3,9 +3,14 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/account"
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
|
||||||
|
"github.com/dustin/go-humanize/english"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
"geeks-accelerator/oss/saas-starter-kit/internal/geonames"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable"
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable"
|
||||||
@ -63,12 +68,7 @@ func (h *Users) Index(ctx context.Context, w http.ResponseWriter, r *http.Reques
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var statusValues []interface{}
|
statusOpts := web.NewEnumResponse(ctx, nil, user_account.UserAccountStatus_ValuesInterface())
|
||||||
for _, v := range user_account.UserAccountStatus_Values {
|
|
||||||
statusValues = append(statusValues, string(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
statusOpts := web.NewEnumResponse(ctx, nil, statusValues...)
|
|
||||||
|
|
||||||
statusFilterItems := []datatable.FilterOptionItem{}
|
statusFilterItems := []datatable.FilterOptionItem{}
|
||||||
for _, opt := range statusOpts.Options {
|
for _, opt := range statusOpts.Options {
|
||||||
@ -497,3 +497,220 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-update.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-update.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invite handles sending invites for users to the account.
|
||||||
|
func (h *Users) Invite(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
ctxValues, err := webcontext.ContextValues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, err := auth.ClaimsFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
req := new(invite.SendUserInvitesRequest)
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() (bool, error) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
if err := decoder.Decode(req, r.PostForm); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.UserID = claims.Subject
|
||||||
|
req.AccountID = claims.Audience
|
||||||
|
|
||||||
|
res, err := invite.SendUserInvites(ctx, claims, h.MasterDB, h.ProjectRoutes.UserInviteAccept, h.NotifyEmail, *req, h.SecretKey, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a success message to the user.
|
||||||
|
inviteCnt := len(res)
|
||||||
|
if inviteCnt > 0 {
|
||||||
|
webcontext.SessionFlashSuccess(ctx,
|
||||||
|
fmt.Sprintf("%s Invited", english.PluralWord(inviteCnt, "User", "")),
|
||||||
|
fmt.Sprintf("%s successfully invited. %s been sent to them to join your account.",
|
||||||
|
english.Plural(inviteCnt, "user", ""),
|
||||||
|
english.PluralWord(inviteCnt, "An email has", "Emails have")))
|
||||||
|
} else {
|
||||||
|
webcontext.SessionFlashWarning(ctx,
|
||||||
|
"Users not Invited",
|
||||||
|
"No users were invited.")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
err = webcontext.ContextSession(ctx).Save(r, w)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, urlUsersIndex(), http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedRoles []interface{}
|
||||||
|
for _, r := range req.Roles {
|
||||||
|
selectedRoles = append(selectedRoles, r.String())
|
||||||
|
}
|
||||||
|
data["roles"] = web.NewEnumMultiResponse(ctx, selectedRoles, user_account.UserAccountRole_ValuesInterface()...)
|
||||||
|
|
||||||
|
data["form"] = req
|
||||||
|
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(invite.SendUserInvitesRequest{})); ok {
|
||||||
|
data["validationDefaults"] = verr.(*weberror.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-invite.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite handles sending invites for users to the account.
|
||||||
|
func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||||
|
|
||||||
|
inviteHash := params["hash"]
|
||||||
|
|
||||||
|
ctxValues, err := webcontext.ContextValues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
req := new(invite.AcceptInviteRequest)
|
||||||
|
data := make(map[string]interface{})
|
||||||
|
f := func() (bool, error) {
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := schema.NewDecoder()
|
||||||
|
if err := decoder.Decode(req, r.PostForm); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the query param value to the request.
|
||||||
|
req.InviteHash = inviteHash
|
||||||
|
|
||||||
|
err = invite.AcceptInvite(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticated the user. Probably should use the default session TTL from UserLogin.
|
||||||
|
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, u.Email, req.Password, time.Hour, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
case account.ErrForbidden:
|
||||||
|
return false, web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
|
||||||
|
default:
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, err); ok {
|
||||||
|
data["validationErrors"] = verr.(*weberror.Error)
|
||||||
|
return false, nil
|
||||||
|
} else {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the token to the users session.
|
||||||
|
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect the user to the dashboard.
|
||||||
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := invite.ParseInviteHash(ctx, h.SecretKey, inviteHash, ctxValues.Now)
|
||||||
|
if err != nil {
|
||||||
|
switch errors.Cause(err) {
|
||||||
|
case invite.ErrInviteExpired:
|
||||||
|
webcontext.SessionFlashError(ctx,
|
||||||
|
"Invite Expired",
|
||||||
|
"The invite has expired.")
|
||||||
|
return false, nil
|
||||||
|
case invite.ErrInviteUserPasswordSet:
|
||||||
|
webcontext.SessionFlashError(ctx,
|
||||||
|
"Invite already Accepted",
|
||||||
|
"The invite has already been accepted. Try to login or use forgot password.")
|
||||||
|
http.Redirect(w, r, "/user/login", http.StatusFound)
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read user by ID with no claims.
|
||||||
|
usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, hash.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
data["user"] = usr.Response(ctx)
|
||||||
|
|
||||||
|
if req.Email == "" {
|
||||||
|
req.FirstName = usr.FirstName
|
||||||
|
req.LastName = usr.LastName
|
||||||
|
req.Email = usr.Email
|
||||||
|
|
||||||
|
if usr.Timezone != "" {
|
||||||
|
req.Timezone = &usr.Timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||||
|
} else if end {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data["form"] = req
|
||||||
|
|
||||||
|
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(invite.AcceptInviteRequest{})); ok {
|
||||||
|
data["validationDefaults"] = verr.(*weberror.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "user-invite-accept.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||||
|
}
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
<label for="selectStatus">Status</label>
|
<label for="selectStatus">Status</label>
|
||||||
<select class="form-control {{ ValidationFieldClass $.validationErrors "Status" }}"
|
<select class="form-control {{ ValidationFieldClass $.validationErrors "Status" }}"
|
||||||
id="selectStatus" name="Status">
|
id="selectStatus" name="Status">
|
||||||
{{ range $idx, $t := .project.Status.Options }}
|
{{ range $t := .project.Status.Options }}
|
||||||
<option value="{{ $t.Value }}" {{ if $t.Selected }}selected="selected"{{ end }}>{{ $t.Title }}</option>
|
<option value="{{ $t.Value }}" {{ if $t.Selected }}selected="selected"{{ end }}>{{ $t.Title }}</option>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</select>
|
</select>
|
||||||
|
65
cmd/web-app/templates/content/users-invite-accept.gohtml
Normal file
65
cmd/web-app/templates/content/users-invite-accept.gohtml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
{{define "title"}}Invite Accept{{end}}
|
||||||
|
{{define "description"}}{{end}}
|
||||||
|
{{define "style"}}
|
||||||
|
|
||||||
|
{{end}}
|
||||||
|
{{ define "partials/app-wrapper" }}
|
||||||
|
<div class="container" id="page-content">
|
||||||
|
|
||||||
|
<!-- Outer Row -->
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
|
||||||
|
<div class="col-xl-10 col-lg-12 col-md-9">
|
||||||
|
|
||||||
|
<div class="card o-hidden border-0 shadow-lg my-5">
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<!-- Nested Row within Card Body -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="p-5">
|
||||||
|
{{ template "app-flashes" . }}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="h4 text-gray-900 mb-2">Invite Accept</h1>
|
||||||
|
<p class="mb-4">.....</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ template "validation-error" . }}
|
||||||
|
|
||||||
|
<form class="user" method="post" novalidate>
|
||||||
|
<input type="hidden" name="ResetHash" value="{{ $.form.ResetHash }}" />
|
||||||
|
<div class="form-group row">
|
||||||
|
<div class="col-sm-6 mb-3 mb-sm-0">
|
||||||
|
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Password" }}" name="Password" value="{{ $.form.Password }}" placeholder="Password" required>
|
||||||
|
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Password" }}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "PasswordConfirm" }}" name="PasswordConfirm" value="{{ $.form.PasswordConfirm }}" placeholder="Repeat Password" required>
|
||||||
|
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "PasswordConfirm" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary btn-user btn-block">
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
<hr>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{define "js"}}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
$(document).find('body').addClass('bg-gradient-primary');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
86
cmd/web-app/templates/content/users-invite.gohtml
Normal file
86
cmd/web-app/templates/content/users-invite.gohtml
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{{define "title"}}Invite Users{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<form method="POST">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputEmail">Email for Invite 1</label>
|
||||||
|
<input type="text" class="form-control invite-user-email" placeholder="enter email" name="Emails" value="">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="selectRoles">Roles</label>
|
||||||
|
<select class="form-control {{ ValidationFieldClass $.validationErrors "Roles" }}"
|
||||||
|
id="selectRoles" name="Roles" multiple="multiple">
|
||||||
|
{{ range $t := .roles }}
|
||||||
|
<option value="{{ $t.Value }}" {{ if $t.Selected }}selected="selected"{{ end }}>{{ $t.Title }}</option>
|
||||||
|
{{ end }}
|
||||||
|
</select>
|
||||||
|
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Roles" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spacer-30"></div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<input id="btnSubmit" type="submit" value="Invite Users" class="btn btn-primary"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
{{ define "js" }}
|
||||||
|
<script>
|
||||||
|
function addAnotherEmail(el) {
|
||||||
|
if ($(el).val() == '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt = 0;
|
||||||
|
$( "input.invite-user-email" ).each(function( index ) {
|
||||||
|
cnt = cnt + 1;
|
||||||
|
});
|
||||||
|
cnt = cnt + 1;
|
||||||
|
|
||||||
|
newId = 'inviteUser'+cnt;
|
||||||
|
newHtml = '';
|
||||||
|
newHtml = newHtml + '<div class="form-group">';
|
||||||
|
newHtml = newHtml + '<label for="inputEmail">Email for Invite '+cnt+'</label>';
|
||||||
|
newHtml = newHtml + '<input type="text" class="form-control invite-user-email" placeholder="enter email" name="Emails" value="">';
|
||||||
|
newHtml = newHtml + '</div>';
|
||||||
|
$(el).closest('div.card-body').append(newHtml);
|
||||||
|
$(el).unbind( "blur" );
|
||||||
|
|
||||||
|
$('#'+newId).on("blur", function() {
|
||||||
|
addAnotherEmail($(this));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function(){
|
||||||
|
$("#inviteUser1").on("blur", function() {
|
||||||
|
addAnotherEmail($(this));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#inputRole").on("change", function() {
|
||||||
|
if ($(this).val() == 'admin') {
|
||||||
|
$('#userProcedures').hide();
|
||||||
|
} else {
|
||||||
|
$('#userProcedures').show();
|
||||||
|
}
|
||||||
|
}).change();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
@ -116,6 +116,34 @@ func NewEnumResponse(ctx context.Context, value interface{}, options ...interfac
|
|||||||
return er
|
return er
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnumResponse is a response friendly format for displaying a multi select enum.
|
||||||
|
type EnumMultiResponse []EnumOption
|
||||||
|
|
||||||
|
// NewEnumMultiResponse returns a display friendly format for a multi enum field.
|
||||||
|
func NewEnumMultiResponse(ctx context.Context, selected []interface{}, options ...interface{}) EnumMultiResponse {
|
||||||
|
var er EnumMultiResponse
|
||||||
|
|
||||||
|
for _, opt := range options {
|
||||||
|
optStr := fmt.Sprintf("%s", opt)
|
||||||
|
opt := EnumOption{
|
||||||
|
Value: optStr,
|
||||||
|
Title: EnumValueTitle(optStr),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range selected {
|
||||||
|
selStr := fmt.Sprintf("%s", s)
|
||||||
|
if optStr == selStr {
|
||||||
|
opt.Selected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
er = append(er, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
// EnumValueTitle formats a string value for display.
|
// EnumValueTitle formats a string value for display.
|
||||||
func EnumValueTitle(v string) string {
|
func EnumValueTitle(v string) string {
|
||||||
v = strings.Replace(v, "_", " ", -1)
|
v = strings.Replace(v, "_", " ", -1)
|
||||||
|
@ -40,8 +40,14 @@ func (r ProjectRoutes) WebApiUrl(urlPath string) string {
|
|||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ProjectRoutes) UserResetPassword(resetId string) string {
|
func (r ProjectRoutes) UserResetPassword(resetHash string) string {
|
||||||
u := r.webAppUrl
|
u := r.webAppUrl
|
||||||
u.Path = "/user/reset-password/" + resetId
|
u.Path = "/user/reset-password/" + resetHash
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ProjectRoutes) UserInviteAccept(inviteHash string) string {
|
||||||
|
u := r.webAppUrl
|
||||||
|
u.Path = "/users/invite/" + inviteHash
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
@ -3,7 +3,6 @@ package invite
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -15,7 +14,6 @@ import (
|
|||||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sudo-suhas/symcrypto"
|
|
||||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -149,30 +147,15 @@ func SendUserInvites(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, r
|
|||||||
|
|
||||||
var inviteHashes []string
|
var inviteHashes []string
|
||||||
for email, userID := range emailUserIDs {
|
for email, userID := range emailUserIDs {
|
||||||
|
hash, err := NewInviteHash(ctx, secretKey, userID, requestIp, req.TTL, now)
|
||||||
// Generate a string that embeds additional information.
|
|
||||||
hashPts := []string{
|
|
||||||
userID,
|
|
||||||
strconv.Itoa(int(now.UTC().Unix())),
|
|
||||||
strconv.Itoa(int(now.UTC().Add(req.TTL).Unix())),
|
|
||||||
requestIp,
|
|
||||||
}
|
|
||||||
hashStr := strings.Join(hashPts, "|")
|
|
||||||
|
|
||||||
// This returns the nonce appended with the encrypted string.
|
|
||||||
crypto, err := symcrypto.New(secretKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithStack(err)
|
return nil, err
|
||||||
}
|
|
||||||
encrypted, err := crypto.Encrypt(hashStr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"FromUser": fromUser.Response(ctx),
|
"FromUser": fromUser.Response(ctx),
|
||||||
"Account": account.Response(ctx),
|
"Account": account.Response(ctx),
|
||||||
"Url": resetUrl(encrypted),
|
"Url": resetUrl(hash),
|
||||||
"Minutes": req.TTL.Minutes(),
|
"Minutes": req.TTL.Minutes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +167,7 @@ func SendUserInvites(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, r
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteHashes = append(inviteHashes, encrypted)
|
inviteHashes = append(inviteHashes, hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
return inviteHashes, nil
|
return inviteHashes, nil
|
||||||
@ -203,32 +186,8 @@ func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
crypto, err := symcrypto.New(secretKey)
|
hash, err := ParseInviteHash(ctx, secretKey, req.InviteHash, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
hashStr, err := crypto.Decrypt(req.InviteHash)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
hashPts := strings.Split(hashStr, "|")
|
|
||||||
|
|
||||||
var hash InviteHash
|
|
||||||
if len(hashPts) == 4 {
|
|
||||||
hash.UserID = hashPts[0]
|
|
||||||
hash.CreatedAt, _ = strconv.Atoi(hashPts[1])
|
|
||||||
hash.ExpiresAt, _ = strconv.Atoi(hashPts[2])
|
|
||||||
hash.RequestIP = hashPts[3]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the hash.
|
|
||||||
err = v.StructCtx(ctx, hash)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if int64(hash.ExpiresAt) < now.UTC().Unix() {
|
|
||||||
err = errors.WithMessage(ErrInviteExpired, "Invite has expired.")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +210,7 @@ func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest,
|
|||||||
|
|
||||||
err = user.Update(ctx, auth.Claims{}, dbConn, user.UserUpdateRequest{
|
err = user.Update(ctx, auth.Claims{}, dbConn, user.UserUpdateRequest{
|
||||||
ID: hash.UserID,
|
ID: hash.UserID,
|
||||||
|
Email: &req.Email,
|
||||||
FirstName: &req.FirstName,
|
FirstName: &req.FirstName,
|
||||||
LastName: &req.LastName,
|
LastName: &req.LastName,
|
||||||
Timezone: req.Timezone,
|
Timezone: req.Timezone,
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
package invite
|
package invite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/sudo-suhas/symcrypto"
|
||||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -26,9 +32,69 @@ type InviteHash struct {
|
|||||||
// AcceptInviteRequest defines the fields need to complete an invite request.
|
// AcceptInviteRequest defines the fields need to complete an invite request.
|
||||||
type AcceptInviteRequest struct {
|
type AcceptInviteRequest struct {
|
||||||
InviteHash string `json:"invite_hash" validate:"required" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
InviteHash string `json:"invite_hash" validate:"required" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
|
||||||
|
Email string `json:"email" validate:"required,email" example:"gabi@geeksinthewoods.com"`
|
||||||
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
|
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
|
||||||
LastName string `json:"last_name" validate:"required" example:"May"`
|
LastName string `json:"last_name" validate:"required" example:"May"`
|
||||||
Password string `json:"password" validate:"required" example:"SecretString"`
|
Password string `json:"password" validate:"required" example:"SecretString"`
|
||||||
PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"`
|
PasswordConfirm string `json:"password_confirm" validate:"required,eqfield=Password" example:"SecretString"`
|
||||||
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
Timezone *string `json:"timezone,omitempty" validate:"omitempty" example:"America/Anchorage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewInviteHash generates a new encrypt invite hash that is web safe for use in URLs.
|
||||||
|
func NewInviteHash(ctx context.Context, secretKey string, userID, requestIp string, ttl time.Duration, now time.Time) (string, error) {
|
||||||
|
// Generate a string that embeds additional information.
|
||||||
|
hashPts := []string{
|
||||||
|
userID,
|
||||||
|
strconv.Itoa(int(now.UTC().Unix())),
|
||||||
|
strconv.Itoa(int(now.UTC().Add(ttl).Unix())),
|
||||||
|
requestIp,
|
||||||
|
}
|
||||||
|
hashStr := strings.Join(hashPts, "|")
|
||||||
|
|
||||||
|
|
||||||
|
// This returns the nonce appended with the encrypted string.
|
||||||
|
crypto, err := symcrypto.New(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
encrypted, err := crypto.Encrypt(hashStr)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return encrypted, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseInviteHash extracts the details encrypted in the hash string.
|
||||||
|
func ParseInviteHash(ctx context.Context, secretKey string, str string, now time.Time) (*InviteHash, error) {
|
||||||
|
crypto, err := symcrypto.New(secretKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
hashStr, err := crypto.Decrypt(str)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
hashPts := strings.Split(hashStr, "|")
|
||||||
|
|
||||||
|
var hash InviteHash
|
||||||
|
if len(hashPts) == 4 {
|
||||||
|
hash.UserID = hashPts[0]
|
||||||
|
hash.CreatedAt, _ = strconv.Atoi(hashPts[1])
|
||||||
|
hash.ExpiresAt, _ = strconv.Atoi(hashPts[2])
|
||||||
|
hash.RequestIP = hashPts[3]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the hash.
|
||||||
|
err = webcontext.Validator().StructCtx(ctx, hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if int64(hash.ExpiresAt) < now.UTC().Unix() {
|
||||||
|
err = errors.WithMessage(ErrInviteExpired, "Invite has expired.")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &hash, nil
|
||||||
|
}
|
||||||
|
@ -328,6 +328,10 @@ func (m *User) Response(ctx context.Context) *UserResponse {
|
|||||||
Gravatar: web.NewGravatarResponse(ctx, m.Email),
|
Gravatar: web.NewGravatarResponse(ctx, m.Email),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.Name == "" {
|
||||||
|
r.Name = r.Email
|
||||||
|
}
|
||||||
|
|
||||||
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
|
||||||
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
|
||||||
r.ArchivedAt = &at
|
r.ArchivedAt = &at
|
||||||
|
Reference in New Issue
Block a user