Commit 33a7fb14 authored by Lee Brown's avatar Lee Brown

Completed user invite

parent 9fbab53b
......@@ -49,7 +49,7 @@ func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Req
return err
}
statusOpts := web.NewEnumResponse(ctx, nil, project.ProjectStatus_ValuesInterface()...)
statusOpts := web.NewEnumResponse(ctx, nil, project.ProjectStatus_ValuesInterface()...)
statusFilterItems := []datatable.FilterOptionItem{}
for _, opt := range statusOpts.Options {
......
......@@ -58,12 +58,12 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
if r.Method == http.MethodPost {
err := r.ParseForm()
if err != nil {
return false,err
return false, err
}
decoder := schema.NewDecoder()
if err := decoder.Decode(req, r.PostForm); err != nil {
return false,err
return false, err
}
sessionTTL := time.Hour
......@@ -76,16 +76,16 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
if err != nil {
switch errors.Cause(err) {
case user.ErrForbidden:
return false,web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
return false, web.RespondError(ctx, w, weberror.NewError(ctx, err, http.StatusForbidden))
case user_auth.ErrAuthenticationFailure:
data["error"] = weberror.NewErrorMessage(ctx, err, http.StatusUnauthorized, "Authentication failure. Try again.")
return false, nil
default:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return false,nil
return false, nil
} else {
return false,err
return false, err
}
}
}
......@@ -93,14 +93,14 @@ func (h *User) Login(ctx context.Context, w http.ResponseWriter, r *http.Request
// Add the token to the users session.
err = handleSessionToken(ctx, h.MasterDB, w, r, token)
if err != nil {
return false,err
return false, err
}
redirectUri := "/"
if qv := r.URL.Query().Get("redirect"); qv != "" {
redirectUri, err = url.QueryUnescape(qv)
if err != nil {
return false,err
return false, err
}
}
......@@ -426,7 +426,7 @@ func (h *User) Update(ctx context.Context, w http.ResponseWriter, r *http.Reques
req.FirstName = &usr.FirstName
req.LastName = &usr.LastName
req.Email = &usr.Email
req.Timezone = &usr.Timezone
req.Timezone = usr.Timezone
}
data["user"] = usr.Response(ctx)
......
......@@ -14,6 +14,7 @@ import (
"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/invite"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
"github.com/dustin/go-humanize/english"
"github.com/gorilla/schema"
"github.com/jmoiron/sqlx"
......@@ -21,6 +22,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
"net/http"
"strings"
"time"
)
// 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
}
statusOpts := web.NewEnumResponse(ctx, nil, user_account.UserAccountStatus_ValuesInterface())
statusOpts := web.NewEnumResponse(ctx, nil, user_account.UserAccountStatus_ValuesInterface()...)
statusFilterItems := []datatable.FilterOptionItem{}
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.LastName = &usr.LastName
req.Email = &usr.Email
req.Timezone = &usr.Timezone
req.Timezone = usr.Timezone
}
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.")
}
err = webcontext.ContextSession(ctx).Save(r, w)
if err != nil {
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.
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 {
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:
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
......@@ -629,20 +651,20 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
}
}
/*
// Load the user without any claims applied.
usr, err := user.ReadByID(ctx, auth.Claims{}, h.MasterDB, userID)
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, u.Email, req.Password, time.Hour, ctxValues.Now)
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, usr.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
}
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return false, nil
} else {
return false, err
}
}
......@@ -651,15 +673,13 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
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)
hash, err := invite.ParseInviteHash(ctx, h.SecretKey, inviteHash, ctxValues.Now)
if err != nil {
switch errors.Cause(err) {
case invite.ErrInviteExpired:
......@@ -674,7 +694,12 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
http.Redirect(w, r, "/user/login", http.StatusFound)
return true, nil
default:
return false, err
if verr, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = verr.(*weberror.Error)
return false, nil
} else {
return false, err
}
}
}
......@@ -689,10 +714,7 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
req.FirstName = usr.FirstName
req.LastName = usr.LastName
req.Email = usr.Email
if usr.Timezone != "" {
req.Timezone = &usr.Timezone
}
req.Timezone = usr.Timezone
}
return false, nil
......@@ -705,11 +727,16 @@ func (h *Users) InviteAccept(ctx context.Context, w http.ResponseWriter, r *http
return nil
}
data["timezones"], err = geonames.ListTimezones(ctx, h.MasterDB)
if err != nil {
return err
}
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)
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "users-invite-accept.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
......@@ -28,22 +28,67 @@
{{ 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 class="card shadow">
<div class="card-body">
<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" }}
</div>
<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" }}
</div>
</div>
</div>
<div class="row">
<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>
<button class="btn btn-primary btn-user btn-block">
Join
</button>
<hr>
</form>
</div>
</div>
</div>
......@@ -58,8 +103,25 @@
{{end}}
{{define "js"}}
<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).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>
{{end}}
\ No newline at end of file
......@@ -121,7 +121,7 @@ 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
var er EnumMultiResponse
for _, opt := range options {
optStr := fmt.Sprintf("%s", opt)
......@@ -137,7 +137,6 @@ func NewEnumMultiResponse(ctx context.Context, selected []interface{}, options .
}
}
er = append(er, opt)
}
......
......@@ -50,4 +50,4 @@ func (r ProjectRoutes) UserInviteAccept(inviteHash string) string {
u := r.webAppUrl
u.Path = "/users/invite/" + inviteHash
return u.String()
}
\ No newline at end of file
}
......@@ -585,5 +585,25 @@ func migrationList(db *sqlx.DB, log *log.Logger, isUnittest bool) []*sqlxmigrate
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
},
},
}
}
......@@ -19,7 +19,7 @@ type User struct {
PasswordSalt string `json:"-" validate:"required"`
PasswordHash []byte `json:"-" validate:"required"`
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"`
UpdatedAt time.Time `json:"updated_at"`
ArchivedAt *pq.NullTime `json:"archived_at,omitempty"`
......@@ -52,12 +52,15 @@ func (m *User) Response(ctx context.Context) *UserResponse {
FirstName: m.FirstName,
LastName: m.LastName,
Email: m.Email,
Timezone: m.Timezone,
CreatedAt: web.NewTimeResponse(ctx, m.CreatedAt),
UpdatedAt: web.NewTimeResponse(ctx, m.UpdatedAt),
Gravatar: web.NewGravatarResponse(ctx, m.Email),
}
if m.Timezone != nil {
r.Timezone = *m.Timezone
}
if m.ArchivedAt != nil && !m.ArchivedAt.Time.IsZero() {
at := web.NewTimeResponse(ctx, m.ArchivedAt.Time)
r.ArchivedAt = &at
......
......@@ -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")
defer span.Finish()
if req.Timezone != nil && *req.Timezone == "" {
req.Timezone = nil
}
v := webcontext.Validator()
// 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,
LastName: req.LastName,
Email: req.Email,
Timezone: req.Timezone,
PasswordHash: passwordHash,
PasswordSalt: passwordSalt,
Timezone: "America/Anchorage",
CreatedAt: now,
UpdatedAt: now,
}
if req.Timezone != nil {
u.Timezone = *req.Timezone
}
// Build the insert SQL statement.
query := sqlbuilder.NewInsertBuilder()
query.InsertInto(userTableName)
......@@ -542,8 +542,8 @@ func Update(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserUp
if req.Email != nil {
fields = append(fields, query.Assign("email", req.Email))
}
if req.Timezone != nil {
fields = append(fields, query.Assign("timezone", req.Timezone))
if req.Timezone != nil && *req.Timezone != "" {
fields = append(fields, query.Assign("timezone", *req.Timezone))
}
// If there's nothing to update we can quit early.
......
......@@ -21,6 +21,12 @@ var (
// ErrInviteExpired occurs when the the reset hash exceeds the expiration.
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 = errors.New("User password set")
)
......@@ -147,7 +153,7 @@ func SendUserInvites(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, r
var inviteHashes []string
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 {
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.
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")
defer span.Finish()
......@@ -183,40 +189,59 @@ func AcceptInvite(ctx context.Context, dbConn *sqlx.DB, req AcceptInviteRequest,
// Validate the request.
err := v.StructCtx(ctx, req)
if err != nil {
return err
return "", err
}
hash, err := ParseInviteHash(ctx, secretKey, req.InviteHash, now)
if err != nil {
return err
return "", err
}
u, err := user.Read(ctx, auth.Claims{}, dbConn,
user.UserReadRequest{ID: hash.UserID, IncludeArchived: true})
if err != nil {
return err
return "", err
}
if u.ArchivedAt != nil && !u.ArchivedAt.Time.IsZero() {
err = user.Restore(ctx, auth.Claims{}, dbConn, user.UserRestoreRequest{ID: hash.UserID}, now)
if err != nil {
return err
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)
}
} else if len(u.PasswordHash) > 0 {
return "", errors.WithStack(ErrNoPendingInvite)
}
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
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{
ID: hash.UserID,
Email: &req.Email,
Email: &req.Email,
FirstName: &req.FirstName,
LastName: &req.LastName,
Timezone: req.Timezone,
}, now)
if err != nil {
return err
return "", err
}
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,
}, now)
if err != nil {
return err
return "", err
}
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 nil
return hash.UserID, nil
}
......@@ -2,14 +2,15 @@ package invite
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"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/sudo-suhas/symcrypto"
"geeks-accelerator/oss/saas-starter-kit/internal/user_account"
)
// SendUserInvitesRequest defines the data needed to make an invite request.
......@@ -24,6 +25,7 @@ type SendUserInvitesRequest struct {
// InviteHash
type InviteHash struct {
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"`
ExpiresAt int `json:"expires_at" validate:"required"`
RequestIP string `json:"request_ip" validate:"required,ip" example:"69.56.104.36"`
......@@ -32,7 +34,7 @@ type InviteHash struct {
// AcceptInviteRequest defines the fields need to complete an invite request.
type AcceptInviteRequest struct {
InviteHash string `json:"invite_hash" validate:"required" example:"d69bdef7-173f-4d29-b52c-3edc60baf6a2"`
Email string `json:"email" validate:"required,email" example:"gabi@geeksinthewoods.com"`
Email string `json:"email" validate:"required,email" example:"gabi@geeksinthewoods.com"`
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
LastName string `json:"last_name" validate:"required" example:"May"`
Password string `json:"password" validate:"required" example:"SecretString"`
......@@ -41,17 +43,17 @@ type AcceptInviteRequest struct {
}
// 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.
hashPts := []string{
userID,
accountID,
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 {
......@@ -77,12 +79,15 @@ func ParseInviteHash(ctx context.Context, secretKey string, str string, now time
}
hashPts := strings.Split(hashStr, "|")
fmt.Println(hashPts)
var hash InviteHash
if len(hashPts) == 4 {
if len(hashPts) == 5 {
hash.UserID = hashPts[0]
hash.CreatedAt, _ = strconv.Atoi(hashPts[1])
hash.ExpiresAt, _ = strconv.Atoi(hashPts[2])
hash.RequestIP = hashPts[3]
hash.AccountID = hashPts[1]
hash.CreatedAt, _ = strconv.Atoi(hashPts[2])
hash.ExpiresAt, _ = strconv.Atoi(hashPts[3])
hash.RequestIP = hashPts[4]
}
// Validate the hash.
......
......@@ -280,7 +280,7 @@ type User struct {
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
LastName string `json:"last_name" validate:"required" example:"May"`
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"`
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"`
......@@ -319,7 +319,6 @@ func (m *User) Response(ctx context.Context) *UserResponse {
FirstName: m.FirstName,
LastName: m.LastName,
Email: m.Email,
Timezone: m.Timezone,
AccountID: m.AccountID,
Roles: m.Roles,
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),
}
if m.Timezone != nil {
r.Timezone = *m.Timezone
}
if r.Name == "" {
r.Name = r.Email
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment