Commit ba96e8b3 authored by Lee Brown's avatar Lee Brown

Completed virtual user login and switch accounts

parent bb9820ff
......@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
"net/http"
"net/http/httptest"
"testing"
......@@ -17,6 +16,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"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_auth"
"github.com/pborman/uuid"
)
......
......@@ -19,8 +19,9 @@ import (
// Account represents the Account API method handler set.
type Account struct {
MasterDB *sqlx.DB
Renderer web.Renderer
MasterDB *sqlx.DB
Renderer web.Renderer
Authenticator *auth.Authenticator
}
// View handles displaying the current account profile.
......@@ -34,7 +35,7 @@ func (h *Account) View(ctx context.Context, w http.ResponseWriter, r *http.Reque
return err
}
acc, err := account.Read(ctx, claims, h.MasterDB, claims.Audience, false)
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience)
if err != nil {
return err
}
......@@ -127,7 +128,11 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
}
}
sess := webcontext.ContextSession(ctx)
var updateClaims bool
if req.Timezone != nil && claims.Preferences.Timezone != *req.Timezone {
claims.Preferences.Timezone = *req.Timezone
updateClaims = true
}
if preferenceDatetimeFormat != req.PreferenceDatetimeFormat {
err = account_preference.Set(ctx, claims, h.MasterDB, account_preference.AccountPreferenceSetRequest{
......@@ -144,7 +149,10 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
}
}
sess.Values[webcontext.SessionKeyPreferenceDatetimeFormat] = req.PreferenceDatetimeFormat
if claims.Preferences.DatetimeFormat != req.PreferenceDatetimeFormat {
claims.Preferences.DatetimeFormat = req.PreferenceDatetimeFormat
updateClaims = true
}
}
if preferenceDateFormat != req.PreferenceDateFormat {
......@@ -162,7 +170,10 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
}
}
sess.Values[webcontext.SessionKeyPreferenceDateFormat] = req.PreferenceDateFormat
if claims.Preferences.DateFormat != req.PreferenceDateFormat {
claims.Preferences.DateFormat = req.PreferenceDateFormat
updateClaims = true
}
}
if preferenceTimeFormat != req.PreferenceTimeFormat {
......@@ -180,7 +191,18 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
}
}
sess.Values[webcontext.SessionKeyPreferenceTimeFormat] = req.PreferenceTimeFormat
if claims.Preferences.TimeFormat != req.PreferenceTimeFormat {
claims.Preferences.TimeFormat = req.PreferenceTimeFormat
updateClaims = true
}
}
// Update the access token to include the updated claims.
if updateClaims {
ctx, err = updateContextClaims(ctx, h.Authenticator, claims)
if err != nil {
return false, err
}
}
// Display a success message to the user.
......@@ -196,7 +218,7 @@ func (h *Account) Update(ctx context.Context, w http.ResponseWriter, r *http.Req
return true, nil
}
acc, err := account.Read(ctx, claims, h.MasterDB, claims.Audience, false)
acc, err := account.ReadByID(ctx, claims, h.MasterDB, claims.Audience)
if err != nil {
return false, err
}
......
......@@ -66,13 +66,21 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
app.Handle("POST", "/user/update", u.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("GET", "/user/update", u.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("GET", "/user/account", u.Account, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("GET", "/user/virtual-login/:user_id", u.VirtualLogin, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("POST", "/user/virtual-login", u.VirtualLogin, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/user/virtual-login", u.VirtualLogin, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/user/virtual-logout", u.VirtualLogout, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("GET", "/user/switch-account/:account_id", u.SwitchAccount, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("POST", "/user/switch-account", u.SwitchAccount, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("GET", "/user/switch-account", u.SwitchAccount, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("POST", "/user", u.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
app.Handle("GET", "/user", u.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth())
// Register account management endpoints.
acc := Account{
MasterDB: masterDB,
Renderer: renderer,
MasterDB: masterDB,
Renderer: renderer,
Authenticator: authenticator,
}
app.Handle("POST", "/account/update", acc.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
app.Handle("GET", "/account/update", acc.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin))
......
......@@ -12,7 +12,7 @@ import (
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"geeks-accelerator/oss/saas-starter-kit/internal/signup"
"geeks-accelerator/oss/saas-starter-kit/internal/user"
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
"github.com/gorilla/schema"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
......@@ -68,7 +68,7 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
}
// Authenticated the new user.
token, err := user.Authenticate(ctx, h.MasterDB, h.Authenticator, req.User.Email, req.User.Password, time.Hour, ctxValues.Now)
token, err := user_auth.Authenticate(ctx, h.MasterDB, h.Authenticator, req.User.Email, req.User.Password, time.Hour, ctxValues.Now)
if err != nil {
return err
}
......@@ -93,13 +93,6 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
return nil
}
data["geonameCountries"] = geonames.ValidGeonameCountries
data["countries"], err = geonames.FindCountries(ctx, h.MasterDB, "name", "")
if err != nil {
return err
}
return nil
}
......@@ -107,6 +100,13 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
}
data["geonameCountries"] = geonames.ValidGeonameCountries
data["countries"], err = geonames.FindCountries(ctx, h.MasterDB, "name", "")
if err != nil {
return err
}
data["form"] = req
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(signup.SignupRequest{})); ok {
......
This diff is collapsed.
......@@ -698,7 +698,7 @@ func main() {
return nil
}
usr, err := user.Read(ctx, auth.Claims{}, masterDb, claims.Subject, false)
usr, err := user.ReadByID(ctx, auth.Claims{}, masterDb, claims.Subject)
if err != nil {
return nil
}
......@@ -733,7 +733,7 @@ func main() {
return nil
}
acc, err := account.Read(ctx, auth.Claims{}, masterDb, claims.Audience, false)
acc, err := account.ReadByID(ctx, auth.Claims{}, masterDb, claims.Audience)
if err != nil {
return nil
}
......@@ -746,6 +746,26 @@ func main() {
return a
},
"ContextCanSwitchAccount": func(ctx context.Context) bool {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil || len(claims.AccountIDs) < 2 {
return false
}
return true
},
"ContextIsVirtualSession": func(ctx context.Context) bool {
claims, err := auth.ClaimsFromContext(ctx)
if err != nil {
return false
}
if claims.RootUserID != "" && claims.RootUserID != claims.Subject {
return true
}
if claims.RootAccountID != "" && claims.RootAccountID != claims.Audience {
return true
}
return false
},
}
imgUrlFormatter := staticUrlFormatter
......@@ -827,11 +847,8 @@ func main() {
switch statusCode {
case http.StatusUnauthorized:
// Handle expired sessions that are returned from the auth middleware.
if strings.Contains(errors.Cause(er).Error(), "token is expired") {
http.Redirect(w, r, "/user/login", http.StatusFound)
return nil
}
http.Redirect(w, r, "/user/login?redirect="+url.QueryEscape(r.RequestURI), http.StatusFound)
return nil
}
return web.RenderError(ctx, w, r, er, renderer, handlers.TmplLayoutBase, handlers.TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
......
{{define "title"}}Switch Account{{end}}
{{define "style"}}
{{end}}
{{ define "partials/page-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-4">Switch Account</h1>
</div>
{{ template "validation-error" . }}
<form class="user" method="post" novalidate>
<div class="form-group">
<select class="form-control form-control-select-box {{ ValidationFieldClass $.validationErrors "AccountID" }}" name="AccountID" placeholder="AccountID" required>
{{ range $i := $.accounts }}
<option value="{{ $i.ID }}" {{ if eq $.form.AccountID $i.ID }}selected="selected"{{ end }}>{{ $i.Name }}</option>
{{ end }}
</select>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "AccountID" }}
</div>
<button class="btn btn-primary btn-user btn-block">
Login
</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}}
{{define "title"}}Switch User{{end}}
{{define "style"}}
{{end}}
{{ define "partials/page-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-4">Switch User</h1>
</div>
{{ template "validation-error" . }}
<form class="user" method="post" novalidate>
<div class="form-group">
<select class="form-control form-control-select-box {{ ValidationFieldClass $.validationErrors "User" }}" name="UserID" placeholder="UserID" required>
{{ range $i := $.users }}
<option value="{{ $i.ID }}" {{ if eq $.form.UserID $i.ID }}selected="selected"{{ end }}>{{ $i.Name }}</option>
{{ end }}
</select>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "UserID" }}
</div>
<button class="btn btn-primary btn-user btn-block">
Login
</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}}
......@@ -177,7 +177,6 @@
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Account Settings
</a>
<a class="dropdown-item" href="/users">
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Manage Users
......@@ -197,8 +196,22 @@
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
Support
</a>
<div class="dropdown-divider"></div>
{{ if ContextCanSwitchAccount $._Ctx }}
<a class="dropdown-item" href="/user/switch-account?modal=1">
<i class="far fa-sign-in fa-sm fa-fw mr-2 text-gray-400"></i>
Switch Account
</a>
{{ end }}
{{ if ContextIsVirtualSession $._Ctx }}
<a class="dropdown-item" href="/user/virtual-logout">
<i class="far fa-sign-out fa-sm fa-fw mr-2 text-gray-400"></i>
Switch Back
</a>
{{ end }}
<a class="dropdown-item" href="/user/logout" data-toggle="modal" data-target="#logoutModal">
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
Logout
......
......@@ -150,7 +150,7 @@ func selectQuery() *sqlbuilder.SelectBuilder {
// Find gets all the accounts from the database based on the request params.
// TODO: Need to figure out why can't parse the args when appending the where
// to the query.
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AccountFindRequest) ([]*Account, error) {
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AccountFindRequest) (Accounts, error) {
query := selectQuery()
if req.Where != nil {
......@@ -170,7 +170,7 @@ func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req AccountF
}
// find internal method for getting all the accounts from the database using a select query.
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, args []interface{}, includedArchived bool) ([]*Account, error) {
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, args []interface{}, includedArchived bool) (Accounts, error) {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.account.Find")
defer span.Finish()
......
......@@ -99,6 +99,21 @@ func (m *AccountResponse) MarshalBinary() ([]byte, error) {
return json.Marshal(m)
}
// Accounts a list of Accounts.
type Accounts []*Account
// Response transforms a list of Accounts to a list of AccountResponses.
func (m *Accounts) Response(ctx context.Context) []*AccountResponse {
var l []*AccountResponse
if m != nil && len(*m) > 0 {
for _, n := range *m {
l = append(l, n.Response(ctx))
}
}
return l
}
// AccountCreateRequest contains information needed to create a new Account.
type AccountCreateRequest struct {
Name string `json:"name" validate:"required,unique" example:"Company Name"`
......
......@@ -23,9 +23,11 @@ const Key ctxKey = 1
// Claims represents the authorization claims transmitted via a JWT.
type Claims struct {
AccountIds []string `json:"accounts"`
Roles []string `json:"roles"`
Preferences ClaimPreferences `json:"prefs"`
RootUserID string `json:"root_user_id"`
RootAccountID string `json:"root_account_id"`
AccountIDs []string `json:"accounts"`
Roles []string `json:"roles"`
Preferences ClaimPreferences `json:"prefs"`
jwt.StandardClaims
}
......@@ -41,14 +43,16 @@ type ClaimPreferences struct {
// NewClaims constructs a Claims value for the identified user. The Claims
// expire within a specified duration of the provided time. Additional fields
// of the Claims can be set after calling NewClaims is desired.
func NewClaims(userId, accountId string, accountIds []string, roles []string, prefs ClaimPreferences, now time.Time, expires time.Duration) Claims {
func NewClaims(userID, accountID string, accountIDs []string, roles []string, prefs ClaimPreferences, now time.Time, expires time.Duration) Claims {
c := Claims{
AccountIds: accountIds,
Roles: roles,
Preferences: prefs,
AccountIDs: accountIDs,
RootAccountID: accountID,
RootUserID: userID,
Roles: roles,
Preferences: prefs,
StandardClaims: jwt.StandardClaims{
Subject: userId,
Audience: accountId,
Subject: userID,
Audience: accountID,
IssuedAt: now.Unix(),
ExpiresAt: now.Add(expires).Unix(),
},
......
......@@ -14,39 +14,23 @@ const KeySession ctxKeySession = 1
// Session keys used to store values.
const (
SessionKeyAccessToken = iota
//SessionKeyPreferenceDatetimeFormat
//SessionKeyPreferenceDateFormat
//SessionKeyPreferenceTimeFormat
//SessionKeyTimezone
)
func init() {
//gob.Register(&Session{})
}
// Session represents a user with authentication.
type Session struct {
*sessions.Session
}
// ContextWithSession appends a universal translator to a context.
func ContextWithSession(ctx context.Context, session *sessions.Session) context.Context {
return context.WithValue(ctx, KeySession, session)
}
// ContextSession returns the session from a context.
func ContextSession(ctx context.Context) *Session {
if s, ok := ctx.Value(KeySession).(*Session); ok {
func ContextSession(ctx context.Context) *sessions.Session {
if s, ok := ctx.Value(KeySession).(*sessions.Session); ok {
return s
}
return nil
}
func ContextAccessToken(ctx context.Context) (string, bool) {
return ContextSession(ctx).AccessToken()
}
func (sess *Session) AccessToken() (string, bool) {
sess := ContextSession(ctx)
if sess == nil {
return "", false
}
......@@ -56,60 +40,19 @@ func (sess *Session) AccessToken() (string, bool) {
return "", false
}
/*
func(sess *Session) PreferenceDatetimeFormat() (string, bool) {
if sess == nil {
return "", false
}
if sv, ok := sess.Values[SessionKeyPreferenceDatetimeFormat].(string); ok {
return sv, true
}
return "", false
}
func(sess *Session) PreferenceDateFormat() (string, bool) {
if sess == nil {
return "", false
}
if sv, ok := sess.Values[SessionKeyPreferenceDateFormat].(string); ok {
return sv, true
}
return "", false
}
func(sess *Session) PreferenceTimeFormat() (string, bool) {
if sess == nil {
return "", false
}
if sv, ok := sess.Values[SessionKeyPreferenceTimeFormat].(string); ok {
return sv, true
}
return "", false
}
func SessionInit(session *sessions.Session, accessToken string) *sessions.Session {
func(sess *Session) Timezone() (*time.Location, bool) {
if sess != nil {
if sv, ok := sess.Values[SessionKeyTimezone].(*time.Location); ok {
return sv, true
}
}
session.Values[SessionKeyAccessToken] = accessToken
return nil, false
return session
}
*/
func SessionInit(session *Session, accessToken string) *Session {
func SessionUpdateAccessToken(session *sessions.Session, accessToken string) *sessions.Session {
session.Values[SessionKeyAccessToken] = accessToken
//session.Values[SessionKeyPreferenceDatetimeFormat] = datetimeFormat
//session.Values[SessionKeyPreferenceDateFormat] = dateFormat
//session.Values[SessionKeyPreferenceTimeFormat] = timeFormat
//session.Values[SessionKeyTimezone] = timezone
return session
}
func SessionDestroy(session *Session) *Session {
func SessionDestroy(session *sessions.Session) *sessions.Session {
delete(session.Values, SessionKeyAccessToken)
......
......@@ -56,6 +56,21 @@ func (m *Project) Response(ctx context.Context) *ProjectResponse {
return r
}
// Projects a list of Projects.
type Projects []*Project
// Response transforms a list of Projects to a list of ProjectResponses.
func (m *Projects) Response(ctx context.Context) []*ProjectResponse {
var l []*ProjectResponse
if m != nil && len(*m) > 0 {
for _, n := range *m {
l = append(l, n.Response(ctx))
}
}
return l
}
// ProjectCreateRequest contains information needed to create a new Project.
type ProjectCreateRequest struct {
AccountID string `json:"account_id" validate:"required,uuid" example:"c4653bf9-5978-48b7-89c5-95704aebb7e2"`
......
......@@ -124,13 +124,13 @@ func findRequestQuery(req ProjectFindRequest) (*sqlbuilder.SelectBuilder, []inte
}
// Find gets all the projects from the database based on the request params.
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req ProjectFindRequest) ([]*Project, error) {
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req ProjectFindRequest) (Projects, error) {
query, args := findRequestQuery(req)
return find(ctx, claims, dbConn, query, args, req.IncludeArchived)
}
// find internal method for getting all the projects from the database using a select query.
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, args []interface{}, includedArchived bool) ([]*Project, error) {
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, args []interface{}, includedArchived bool) (Projects, error) {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.project.Find")
defer span.Finish()
......
......@@ -78,6 +78,21 @@ func (m *UserResponse) MarshalBinary() ([]byte, error) {
return json.Marshal(m)
}
// Users a list of Users.
type Users []*User
// Response transforms a list of Users to a list of UserResponses.
func (m *Users) Response(ctx context.Context) []*UserResponse {
var l []*UserResponse
if m != nil && len(*m) > 0 {
for _, n := range *m {
l = append(l, n.Response(ctx))
}
}
return l
}
// UserCreateRequest contains information needed to create a new User.
type UserCreateRequest struct {
FirstName string `json:"first_name" validate:"required" example:"Gabi"`
......
......@@ -202,13 +202,13 @@ func findRequestQuery(req UserFindRequest) (*sqlbuilder.SelectBuilder, []interfa
}
// Find gets all the users from the database based on the request params.
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserFindRequest) ([]*User, error) {
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserFindRequest) (Users, error) {
query, args := findRequestQuery(req)
return find(ctx, claims, dbConn, query, args, req.IncludeArchived)
}
// find internal method for getting all the users from the database using a select query.
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, args []interface{}, includedArchived bool) ([]*User, error) {
func find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, query *sqlbuilder.SelectBuilder, args []interface{}, includedArchived bool) (Users, error) {
span, ctx := tracer.StartSpanFromContext(ctx, "internal.user.Find")
defer span.Finish()
......
......@@ -66,6 +66,34 @@ func (m *UserAccount) Response(ctx context.Context) *UserAccountResponse {
return r
}
// HasRole checks if the entry has a role.
func (m *UserAccount) HasRole(role UserAccountRole) bool {
if m == nil {
return false
}
for _, r := range m.Roles {
if r == role {
return true
}
}
return false
}
// UserAccounts a list of UserAccounts.
type UserAccounts []*UserAccount
// Response transforms a list of UserAccounts to a list of UserAccountResponses.
func (m *UserAccounts) Response(ctx context.Context) []*UserAccountResponse {
var l []*UserAccountResponse
if m != nil && len(*m) > 0 {
for _, n := range *m {
l = append(l, n.Response(ctx))
}
}
return l
}
// UserAccountCreateRequest defines the information is needed to associate a user to an
// account. Users are global to the application and each users access can be managed
// on an account level. If a current entry exists in the database but is archived,
......
......@@ -131,13 +131,13 @@ func findRequestQuery(req UserAccountFindRequest) (*sqlbuilder.SelectBuilder, []
}
// Find gets all the user accounts from the database based on the request params.
func Find(ctx context.Context, claims auth.Claims, dbConn *sqlx.DB, req UserAccountFindRequest) ([]*UserAccount, error) {