1
0
mirror of https://github.com/raseels-repos/golang-saas-starter-kit.git synced 2025-08-08 22:36:41 +02:00

Completed user invite

This commit is contained in:
Lee Brown
2019-08-05 14:32:45 -08:00
parent 9fbab53b4c
commit 33a7fb14b6
12 changed files with 238 additions and 84 deletions

View File

@ -426,7 +426,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
req.FirstName = &usr.FirstName req.FirstName = &usr.FirstName
req.LastName = &usr.LastName req.LastName = &usr.LastName
req.Email = &usr.Email req.Email = &usr.Email
req.Timezone = &usr.Timezone req.Timezone = usr.Timezone
} }
data["user"] = usr.Response(ctx) data["user"] = usr.Response(ctx)

View File

@ -14,6 +14,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/user" "geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account" "geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite" "geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
"github.com/dustin/go-humanize/english" "github.com/dustin/go-humanize/english"
"github.com/gorilla/schema" "github.com/gorilla/schema"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -21,6 +22,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
"net/http" "net/http"
"strings" "strings"
"time"
) )
// Users represents the Users API method handler set. // Users represents the Users API method handler set.
@ -64,7 +66,7 @@ func (h *Users) Index(ctx context.Context, w http.ResponseWriter, r *http.Reques
return err return err
} }
statusOpts := web.NewEnumResponse(ctx, nil, user_account.UserAccountStatus_ValuesInterface()) statusOpts := web.NewEnumResponse(ctx, nil, user_account.UserAccountStatus_ValuesInterface()...)
statusFilterItems := []datatable.FilterOptionItem{} statusFilterItems := []datatable.FilterOptionItem{}
for _, opt := range statusOpts.Options { for _, opt := range statusOpts.Options {
@ -471,7 +473,7 @@ func (h *Users) Update(ctx context.Context, w http.ResponseWriter, r *http.Reque
req.FirstName = &usr.FirstName req.FirstName = &usr.FirstName
req.LastName = &usr.LastName req.LastName = &usr.LastName
req.Email = &usr.Email req.Email = &usr.Email
req.Timezone = &usr.Timezone req.Timezone = usr.Timezone
} }
data["user"] = usr.Response(ctx) data["user"] = usr.Response(ctx)
@ -552,7 +554,6 @@ func (h *Users) Invite(ctx context.Context, w http.ResponseWriter, r *http.Reque
"No users were invited.") "No users were invited.")
} }
err = webcontext.ContextSession(ctx).Save(r, w) err = webcontext.ContextSession(ctx).Save(r, w)
if err != nil { if err != nil {
return false, err return false, err
@ -616,9 +617,30 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
// Append the query param value to the request. // Append the query param value to the request.
req.InviteHash = inviteHash req.InviteHash = inviteHash
err = invite.AcceptInvite(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now) userID, err := invite.AcceptInvite(ctx, h.MasterDB, *req, h.SecretKey, ctxValues.Now)
if err != nil { if err != nil {
switch errors.Cause(err) { switch errors.Cause(err) {
case invite.ErrInviteExpired:
webcontext.SessionFlashError(ctx,
"Invite Expired",
"The invite has expired.")
return false, nil
case invite.ErrUserAccountActive:
webcontext.SessionFlashError(ctx,
"User already Active",
"The user already is already active for the account. Try to login or use forgot password.")
http.Redirect(w, r, "/user/login", http.StatusFound)
return true, 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
case user_account.ErrNotFound:
return false, err
case invite.ErrNoPendingInvite:
return false, err
default: default:
if verr, ok := weberror.NewValidationError(ctx, err); ok { if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error) data["validationErrors"] = verr.(*weberror.Error)
@ -629,14 +651,15 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
} }
} }
/* // Load the user without any claims applied.
// Authenticated the user. Probably should use the default session TTL from UserLogin. usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, userID)
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, u.Email, req.Password, time.Hour, ctxValues.Now) if err != nil {
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, usr.Email, req.Password, time.Hour, ctxValues.Now)
if err != nil { 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 { if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error) data["validationErrors"] = verr.(*weberror.Error)
return false, nil return false, nil
@ -644,7 +667,6 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
return false, err return false, err
} }
} }
}
// Add the token to the users session. // Add the token to the users session.
err = handleSessionToken(ctx, h.MasterDB, w, r, token) err = handleSessionToken(ctx, h.MasterDB, w, r, token)
@ -652,8 +674,6 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
return false, err return false, err
} }
*/
// Redirect the user to the dashboard. // Redirect the user to the dashboard.
http.Redirect(w, r, "/", http.StatusFound) http.Redirect(w, r, "/", http.StatusFound)
return true, nil return true, nil
@ -674,9 +694,14 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
http.Redirect(w, r, "/user/login", http.StatusFound) http.Redirect(w, r, "/user/login", http.StatusFound)
return true, nil return true, nil
default: default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return false, nil
} else {
return false, err return false, err
} }
} }
}
// Read user by ID with no claims. // Read user by ID with no claims.
usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, hash.UserID) usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, hash.UserID)
@ -689,10 +714,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
req.FirstName = usr.FirstName req.FirstName = usr.FirstName
req.LastName = usr.LastName req.LastName = usr.LastName
req.Email = usr.Email req.Email = usr.Email
req.Timezone = usr.Timezone
if usr.Timezone != "" {
req.Timezone = &usr.Timezone
}
} }
return false, nil return false, nil
@ -705,11 +727,16 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
return nil return nil
} }
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
if err != nil {
return err
}
data["form"] = req data["form"] = req
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(invite.AcceptInviteRequest{})); ok { if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(invite.AcceptInviteRequest{})); ok {
data["validationDefaults"] = verr.(*weberror.Error) data["validationDefaults"] = verr.(*weberror.Error)
} }
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "user-invite-accept.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-invite-accept.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
} }

View File

@ -28,22 +28,67 @@
{{ template "validation-error" . }} {{ template "validation-error" . }}
<form class="user" method="post" novalidate> <form class="user" method="post" novalidate>
<input type="hidden" name="ResetHash" value="{{ $.form.ResetHash }}" />
<div class="form-group row"> <div class="card shadow">
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="card-body">
<input type="password" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Password" }}" name="Password" value="{{ $.form.Password }}" placeholder="Password" required>
<div class="row mb-2">
<div class="col-md-6">
<div class="form-group">
<label for="inputFirstName">First Name</label>
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "FirstName" }}" placeholder="enter first name" name="FirstName" value="{{ .form.FirstName }}" required>
{{template "invalid-feedback" dict "validationDefaults" $.userValidationDefaults "validationErrors" $.validationErrors "fieldName" "FirstName" }}
</div>
<div class="form-group">
<label for="inputLastName">Last Name</label>
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "LastName" }}" placeholder="enter last name" name="LastName" value="{{ .form.LastName }}" required>
{{template "invalid-feedback" dict "validationDefaults" $.userValidationDefaults "validationErrors" $.validationErrors "fieldName" "LastName" }}
</div>
<div class="form-group">
<label for="inputEmail">Email</label>
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "Email" }}" placeholder="enter email" name="Email" value="{{ .form.Email }}" required>
{{template "invalid-feedback" dict "validationDefaults" $.userValidationDefaults "validationErrors" $.validationErrors "fieldName" "Email" }}
</div>
<div class="form-group">
<label for="inputTimezone">Timezone</label>
<select class="form-control {{ ValidationFieldClass $.validationErrors "Timezone" }}" name="Timezone">
<option value="">Not set</option>
{{ range $idx, $t := .timezones }}
<option value="{{ $t }}" {{ if CmpString $t $.form.Timezone }}selected="selected"{{ end }}>{{ $t }}</option>
{{ end }}
</select>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Timezone" }}
</div>
<div class="form-group">
<label for="inputPassword">Password</label>
<input type="password" class="form-control" id="inputPassword" placeholder="" name="Password" value="">
<span class="help-block "><small><a a href="javascript:void(0)" id="btnGeneratePassword"><i class="fas fa-random mr-1"></i>Generate random password </a></small></span>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Password" }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Password" }}
</div> </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> <div class="form-group">
<label for="inputPasswordConfirm">Confirm Password</label>
<input type="password" class="form-control" id="inputPasswordConfirm" placeholder="" name="PasswordConfirm" value="">
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "PasswordConfirm" }} {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "PasswordConfirm" }}
</div> </div>
</div> </div>
<button class="btn btn-primary btn-user btn-block"> </div>
Join
</button> <div class="row">
<hr> <div class="col">
<input type="submit" value="Save" class="btn btn-primary"/>
<a href="/users/{{ .user.ID }}" class="ml-2 btn btn-secondary" >Cancel</a>
</div>
</div>
</div>
</div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
@ -58,8 +103,25 @@
{{end}} {{end}}
{{define "js"}} {{define "js"}}
<script> <script>
function randomPassword(length) {
var chars = "abcdefghijklmnopqrstuvwxyz!@#&*()-+<>ABCDEFGHIJKLMNOP1234567890";
var pass = "";
for (var x = 0; x < length; x++) {
var i = Math.floor(Math.random() * chars.length);
pass += chars.charAt(i);
}
return pass;
}
$(document).ready(function() { $(document).ready(function() {
$(document).find('body').addClass('bg-gradient-primary'); $(document).find('body').addClass('bg-gradient-primary');
$("#btnGeneratePassword").on("click", function() {
pwd = randomPassword(12);
$("#inputPassword").attr('type', 'text').val(pwd)
$("#inputPasswordConfirm").attr('type', 'text').val(pwd)
return false;
});
}); });
</script> </script>
{{end}} {{end}}

View File

@ -137,7 +137,6 @@ func NewEnumMultiResponse(ctx context.Context, selected []interface{}, options .
} }
} }
er = append(er, opt) er = append(er, opt)
} }

View File

@ -585,5 +585,25 @@ func migrationList(db *sqlx.DB, log *log.Logger, isUnittest bool) []*sqlxmigrate
return nil return nil
}, },
}, },
// Remove default value for users.timezone.
{
ID: "20190805-01",
Migrate: func(tx *sql.Tx) error {
q1 := `ALTER TABLE users ALTER COLUMN timezone DROP DEFAULT`
if _, err := tx.Exec(q1); err != nil {
return errors.WithMessagef(err, "Query failed %s", q1)
}
q2 := `ALTER TABLE users ALTER COLUMN timezone DROP NOT NULL`
if _, err := tx.Exec(q2); err != nil {
return errors.WithMessagef(err, "Query failed %s", q2)
}
return nil
},
Rollback: func(tx *sql.Tx) error {
return nil
},
},
} }
} }

View File

@ -19,7 +19,7 @@ type User struct {
PasswordSalt string `json:"-" validate:"required"` PasswordSalt string `json:"-" validate:"required"`
PasswordHash []byte `json:"-" validate:"required"` PasswordHash []byte `json:"-" validate:"required"`
PasswordReset *sql.NullString `json:"-"` PasswordReset *sql.NullString `json:"-"`
Timezone string `json:"timezone" validate:"omitempty" example:"America/Anchorage"` Timezone *string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"` ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
@ -52,12 +52,15 @@ func (m *User) Response(ctx context.Context) *UserResponse {
FirstName: m.FirstName, FirstName: m.FirstName,
LastName: m.LastName, LastName: m.LastName,
Email: m.Email, Email: m.Email,
Timezone: m.Timezone,
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt), CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt), UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
Gravatar: web.NewGravatarResponse(ctx, m.Email), Gravatar: web.NewGravatarResponse(ctx, m.Email),
} }
if m.Timezone != nil {
r.Timezone = *m.Timezone
}
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

View File

@ -280,6 +280,10 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Create")
defer span.Finish() defer span.Finish()
if req.Timezone != nil && *req.Timezone == "" {
req.Timezone = nil
}
v := webcontext.Validator() v := webcontext.Validator()
// Validation email address is unique in the database. // Validation email address is unique in the database.
@ -330,17 +334,13 @@ func Create(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserCr
FirstName: req.FirstName, FirstName: req.FirstName,
LastName: req.LastName, LastName: req.LastName,
Email: req.Email, Email: req.Email,
Timezone: req.Timezone,
PasswordHash: passwordHash, PasswordHash: passwordHash,
PasswordSalt: passwordSalt, PasswordSalt: passwordSalt,
Timezone: "America/Anchorage",
CreatedAt: now, CreatedAt: now,
UpdatedAt: now, UpdatedAt: now,
} }
if req.Timezone != nil {
u.Timezone = *req.Timezone
}
// Build the insert SQL statement. // Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder() query := sqlbuilder.NewInsertBuilder()
query.InsertInto(userTableName) query.InsertInto(userTableName)
@ -542,8 +542,8 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUp
if req.Email != nil { if req.Email != nil {
fields = append(fields, query.Assign("email", req.Email)) fields = append(fields, query.Assign("email", req.Email))
} }
if req.Timezone != nil { if req.Timezone != nil && *req.Timezone != "" {
fields = append(fields, query.Assign("timezone", req.Timezone)) fields = append(fields, query.Assign("timezone", *req.Timezone))
} }
// If there's nothing to update we can quit early. // If there's nothing to update we can quit early.

View File

@ -21,6 +21,12 @@ var (
// ErrInviteExpired occurs when the the reset hash exceeds the expiration. // ErrInviteExpired occurs when the the reset hash exceeds the expiration.
ErrInviteExpired = errors.New("Invite expired") ErrInviteExpired = errors.New("Invite expired")
// ErrNoPendingInvite occurs when the user does not have an entry in user_accounts with status pending.
ErrNoPendingInvite = errors.New("No pending invite.")
// ErrUserAccountActive occurs when the user already has an active user_account entry.
ErrUserAccountActive = errors.New("User already active.")
// ErrInviteUserPasswordSet occurs when the the reset hash exceeds the expiration. // ErrInviteUserPasswordSet occurs when the the reset hash exceeds the expiration.
ErrInviteUserPasswordSet = errors.New("User password set") ErrInviteUserPasswordSet = errors.New("User password set")
) )
@ -147,7 +153,7 @@ 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) hash, err := NewInviteHash(ctx, secretKey, userID, req.AccountID, requestIp, req.TTL, now)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -174,7 +180,7 @@ func SendUserInvites(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, r
} }
// AcceptInvite updates the user using the provided invite hash. // AcceptInvite updates the user using the provided invite hash.
func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest, secretKey string, now time.Time) error { func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest, secretKey string, now time.Time) (string, error) {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.invite.AcceptInvite") span, ctx := tracer.StartSpanFromContext(ctx, "internal.user_account.invite.AcceptInvite")
defer span.Finish() defer span.Finish()
@ -183,31 +189,50 @@ func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest,
// Validate the request. // Validate the request.
err := v.StructCtx(ctx, req) err := v.StructCtx(ctx, req)
if err != nil { if err != nil {
return err return "", err
} }
hash, err := ParseInviteHash(ctx, secretKey, req.InviteHash, now) hash, err := ParseInviteHash(ctx, secretKey, req.InviteHash, now)
if err != nil { if err != nil {
return err return "", err
} }
u, err := user.Read(ctx, auth.Claims{}, dbConn, u, err := user.Read(ctx, auth.Claims{}, dbConn,
user.UserReadRequest{ID: hash.UserID, IncludeArchived: true}) user.UserReadRequest{ID: hash.UserID, IncludeArchived: true})
if err != nil { if err != nil {
return err return "", err
} }
if u.ArchivedAt != nil && !u.ArchivedAt.Time.IsZero() { if u.ArchivedAt != nil && !u.ArchivedAt.Time.IsZero() {
err = user.Restore(ctx, auth.Claims{}, dbConn, user.UserRestoreRequest{ID: hash.UserID}, now) err = user.Restore(ctx, auth.Claims{}, dbConn, user.UserRestoreRequest{ID: hash.UserID}, now)
if err != nil { if err != nil {
return err return "", err
} }
} else if len(u.PasswordHash) > 0 {
// Do not update the password for a user that already has a password set.
err = errors.WithMessage(ErrInviteUserPasswordSet, "Invite user already has a password set.")
return err
} }
usrAcc, err := user_account.Read(ctx, auth.Claims{}, dbConn, user_account.UserAccountReadRequest{
UserID: hash.UserID,
AccountID: hash.AccountID,
})
if err != nil {
return "", nil
}
// Ensure the entry has the status of invited.
if usrAcc.Status != user_account.UserAccountStatus_Invited {
// If the entry is already active
if usrAcc.Status == user_account.UserAccountStatus_Active {
return u.ID, errors.WithStack(ErrUserAccountActive)
}
return "", errors.WithStack(ErrNoPendingInvite)
}
if len(u.PasswordHash) > 0 {
// Do not update the password for a user that already has a password set.
return "", errors.WithStack(ErrInviteUserPasswordSet)
}
// These two calls, user.Update and user.UpdatePassword should probably be in a transaction!
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, Email: &req.Email,
@ -216,7 +241,7 @@ func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest,
Timezone: req.Timezone, Timezone: req.Timezone,
}, now) }, now)
if err != nil { if err != nil {
return err return "", err
} }
err = user.UpdatePassword(ctx, auth.Claims{}, dbConn, user.UserUpdatePasswordRequest{ err = user.UpdatePassword(ctx, auth.Claims{}, dbConn, user.UserUpdatePasswordRequest{
@ -225,8 +250,18 @@ func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest,
PasswordConfirm: req.PasswordConfirm, PasswordConfirm: req.PasswordConfirm,
}, now) }, now)
if err != nil { if err != nil {
return err return "", err
} }
return nil activeStatus := user_account.UserAccountStatus_Active
err = user_account.Update(ctx, auth.Claims{}, dbConn, user_account.UserAccountUpdateRequest{
UserID: usrAcc.UserID,
AccountID: usrAcc.AccountID,
Status: &activeStatus,
}, now)
if err != nil {
return "", err
}
return hash.UserID, nil
} }

View File

@ -2,14 +2,15 @@ package invite
import ( import (
"context" "context"
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sudo-suhas/symcrypto" "github.com/sudo-suhas/symcrypto"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
) )
// SendUserInvitesRequest defines the data needed to make an invite request. // SendUserInvitesRequest defines the data needed to make an invite request.
@ -24,6 +25,7 @@ type SendUserInvitesRequest struct {
// InviteHash // InviteHash
type InviteHash struct { type InviteHash struct {
UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"` UserID string `json:"user_id" validate:"required,uuid" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
CreatedAt int `json:"created_at" validate:"required"` CreatedAt int `json:"created_at" validate:"required"`
ExpiresAt int `json:"expires_at" validate:"required"` ExpiresAt int `json:"expires_at" validate:"required"`
RequestIP string `json:"request_ip" validate:"required,ip" example:"69.56.104.36"` RequestIP string `json:"request_ip" validate:"required,ip" example:"69.56.104.36"`
@ -41,17 +43,17 @@ type AcceptInviteRequest struct {
} }
// NewInviteHash generates a new encrypt invite hash that is web safe for use in URLs. // 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) { func NewInviteHash(ctx context.Context, secretKey, userID, accountID, requestIp string, ttl time.Duration, now time.Time) (string, error) {
// Generate a string that embeds additional information. // Generate a string that embeds additional information.
hashPts := []string{ hashPts := []string{
userID, userID,
accountID,
strconv.Itoa(int(now.UTC().Unix())), strconv.Itoa(int(now.UTC().Unix())),
strconv.Itoa(int(now.UTC().Add(ttl).Unix())), strconv.Itoa(int(now.UTC().Add(ttl).Unix())),
requestIp, requestIp,
} }
hashStr := strings.Join(hashPts, "|") hashStr := strings.Join(hashPts, "|")
// This returns the nonce appended with the encrypted string. // This returns the nonce appended with the encrypted string.
crypto, err := symcrypto.New(secretKey) crypto, err := symcrypto.New(secretKey)
if err != nil { if err != nil {
@ -77,12 +79,15 @@ func ParseInviteHash(ctx context.Context, secretKey string, str string, now time
} }
hashPts := strings.Split(hashStr, "|") hashPts := strings.Split(hashStr, "|")
fmt.Println(hashPts)
var hash InviteHash var hash InviteHash
if len(hashPts) == 4 { if len(hashPts) == 5 {
hash.UserID = hashPts[0] hash.UserID = hashPts[0]
hash.CreatedAt, _ = strconv.Atoi(hashPts[1]) hash.AccountID = hashPts[1]
hash.ExpiresAt, _ = strconv.Atoi(hashPts[2]) hash.CreatedAt, _ = strconv.Atoi(hashPts[2])
hash.RequestIP = hashPts[3] hash.ExpiresAt, _ = strconv.Atoi(hashPts[3])
hash.RequestIP = hashPts[4]
} }
// Validate the hash. // Validate the hash.

View File

@ -280,7 +280,7 @@ type User struct {
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"`
Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"` Email string `json:"email" validate:"required,email,unique" example:"gabi@geeksinthewoods.com"`
Timezone string `json:"timezone" validate:"omitempty" example:"America/Anchorage"` Timezone *string `json:"timezone" validate:"omitempty" example:"America/Anchorage"`
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"` AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"` Roles UserAccountRoles `json:"roles" validate:"required,dive,oneof=admin user" enums:"admin,user" swaggertype:"array,string" example:"admin"`
Status UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"` Status UserAccountStatus `json:"status" validate:"omitempty,oneof=active invited disabled" enums:"active,invited,disabled" swaggertype:"string" example:"active"`
@ -319,7 +319,6 @@ func (m *User) Response(ctx context.Context) *UserResponse {
FirstName: m.FirstName, FirstName: m.FirstName,
LastName: m.LastName, LastName: m.LastName,
Email: m.Email, Email: m.Email,
Timezone: m.Timezone,
AccountID: m.AccountID, AccountID: m.AccountID,
Roles: m.Roles, Roles: m.Roles,
Status: web.NewEnumResponse(ctx, m.Status, UserAccountStatus_Values), Status: web.NewEnumResponse(ctx, m.Status, UserAccountStatus_Values),
@ -328,6 +327,10 @@ func (m *User) Response(ctx context.Context) *UserResponse {
Gravatar: web.NewGravatarResponse(ctx, m.Email), Gravatar: web.NewGravatarResponse(ctx, m.Email),
} }
if m.Timezone != nil {
r.Timezone = *m.Timezone
}
if r.Name == "" { if r.Name == "" {
r.Name = r.Email r.Name = r.Email
} }