a509ec538b
Adds users.email (optional, lowercase-unique via partial index), smtp_config singleton, and password_resets tokens. Plus the queries U3-T2 / T3 / T4 use: - ChangeUserPassword: self-service password change. HTTP layer verifies the current password before this fires. - UpdateUserProfile: set display_name + email together. - RegenerateApiToken: invalidate old API token. - GetUserByEmail: case-insensitive lookup for forgot-password. - GetSMTPConfig + UpdateSMTPConfig: admin SMTP settings CRUD. - CreatePasswordReset / GetPasswordReset / UsePasswordReset (:execrows for atomic claim) / DeleteExpiredPasswordResets (cron-style cleanup, not yet wired). Email column is nullable; users without email have admin-reset as their only password-recovery path. The lower() unique index allows many NULLs and prevents case-insensitive duplicates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
578 lines
16 KiB
Go
578 lines
16 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: users.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const changeUserPassword = `-- name: ChangeUserPassword :exec
|
|
UPDATE users SET password_hash = $2 WHERE id = $1
|
|
`
|
|
|
|
type ChangeUserPasswordParams struct {
|
|
ID pgtype.UUID
|
|
PasswordHash string
|
|
}
|
|
|
|
// Self-service password change. Caller (HTTP handler) verifies the
|
|
// current password before calling this. Distinct from
|
|
// ResetUserPassword which is admin-driven (no current-password
|
|
// check).
|
|
func (q *Queries) ChangeUserPassword(ctx context.Context, arg ChangeUserPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, changeUserPassword, arg.ID, arg.PasswordHash)
|
|
return err
|
|
}
|
|
|
|
const countAdmins = `-- name: CountAdmins :one
|
|
SELECT count(*) FROM users WHERE is_admin = true
|
|
`
|
|
|
|
func (q *Queries) CountAdmins(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countAdmins)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const countUsers = `-- name: CountUsers :one
|
|
SELECT count(*) FROM users
|
|
`
|
|
|
|
func (q *Queries) CountUsers(ctx context.Context) (int64, error) {
|
|
row := q.db.QueryRow(ctx, countUsers)
|
|
var count int64
|
|
err := row.Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
const createUser = `-- name: CreateUser :one
|
|
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type CreateUserParams struct {
|
|
Username string
|
|
PasswordHash string
|
|
ApiToken string
|
|
IsAdmin bool
|
|
DisplayName *string
|
|
}
|
|
|
|
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, createUser,
|
|
arg.Username,
|
|
arg.PasswordHash,
|
|
arg.ApiToken,
|
|
arg.IsAdmin,
|
|
arg.DisplayName,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createUserAdmin = `-- name: CreateUserAdmin :one
|
|
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type CreateUserAdminParams struct {
|
|
Username string
|
|
PasswordHash string
|
|
ApiToken string
|
|
IsAdmin bool
|
|
DisplayName *string
|
|
}
|
|
|
|
// Admin-driven user creation. Distinct from CreateUser/CreateUserFirstAdminRace:
|
|
// the caller (an admin) supplies all five fields explicitly including is_admin,
|
|
// so an admin can promote on creation. Used by POST /api/admin/users.
|
|
func (q *Queries) CreateUserAdmin(ctx context.Context, arg CreateUserAdminParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, createUserAdmin,
|
|
arg.Username,
|
|
arg.PasswordHash,
|
|
arg.ApiToken,
|
|
arg.IsAdmin,
|
|
arg.DisplayName,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createUserFirstAdminRace = `-- name: CreateUserFirstAdminRace :one
|
|
INSERT INTO users (username, password_hash, api_token, is_admin, display_name)
|
|
VALUES (
|
|
$1, $2, $3,
|
|
(SELECT NOT EXISTS (SELECT 1 FROM users)),
|
|
$4
|
|
)
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type CreateUserFirstAdminRaceParams struct {
|
|
Username string
|
|
PasswordHash string
|
|
ApiToken string
|
|
DisplayName *string
|
|
}
|
|
|
|
// Inserts a new user; sets is_admin=true ONLY when no users currently
|
|
// exist. The (SELECT NOT EXISTS ...) subquery is evaluated at INSERT
|
|
// time within the same statement, so the result reflects committed
|
|
// state at that moment.
|
|
//
|
|
// Race semantics: two concurrent calls in an empty-users state may
|
|
// both observe "no users" and both insert with is_admin=true. That's
|
|
// benign — having two admins from the gate is fine; what matters is
|
|
// that there's AT LEAST one. If the two calls happened to share the
|
|
// same username, the unique constraint on users.username arbitrates
|
|
// and the second caller's INSERT fails with a unique violation. The
|
|
// caller (registration handler) can retry as a regular non-admin in
|
|
// that case (or surface a "username taken" error to the user).
|
|
func (q *Queries) CreateUserFirstAdminRace(ctx context.Context, arg CreateUserFirstAdminRaceParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, createUserFirstAdminRace,
|
|
arg.Username,
|
|
arg.PasswordHash,
|
|
arg.ApiToken,
|
|
arg.DisplayName,
|
|
)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteUser = `-- name: DeleteUser :exec
|
|
DELETE FROM users WHERE id = $1
|
|
`
|
|
|
|
// Hard delete with cascade. The schema's ON DELETE CASCADE foreign keys
|
|
// handle plays/likes/sessions/etc. Caller must enforce the last-admin
|
|
// guard at the HTTP layer (count admins, refuse if target is the only
|
|
// admin). The lidarr_quarantine, general_likes*, play_events,
|
|
// sessions, and other user-FK tables all have ON DELETE CASCADE so
|
|
// this is a clean drop.
|
|
func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteUser, id)
|
|
return err
|
|
}
|
|
|
|
const getListenBrainzConfig = `-- name: GetListenBrainzConfig :one
|
|
SELECT
|
|
u.listenbrainz_token,
|
|
u.listenbrainz_enabled,
|
|
(SELECT MAX(pe.scrobbled_at)::timestamptz
|
|
FROM play_events pe
|
|
WHERE pe.user_id = u.id) AS last_scrobbled_at
|
|
FROM users u
|
|
WHERE u.id = $1
|
|
`
|
|
|
|
type GetListenBrainzConfigRow struct {
|
|
ListenbrainzToken *string
|
|
ListenbrainzEnabled bool
|
|
LastScrobbledAt pgtype.Timestamptz
|
|
}
|
|
|
|
// Returns the user's LB token + enabled flag and the most recent
|
|
// play_events.scrobbled_at for last-scrobbled-at status.
|
|
func (q *Queries) GetListenBrainzConfig(ctx context.Context, id pgtype.UUID) (GetListenBrainzConfigRow, error) {
|
|
row := q.db.QueryRow(ctx, getListenBrainzConfig, id)
|
|
var i GetListenBrainzConfigRow
|
|
err := row.Scan(&i.ListenbrainzToken, &i.ListenbrainzEnabled, &i.LastScrobbledAt)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByAPIToken = `-- name: GetUserByAPIToken :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email FROM users WHERE api_token = $1
|
|
`
|
|
|
|
func (q *Queries) GetUserByAPIToken(ctx context.Context, apiToken string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByAPIToken, apiToken)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByEmail = `-- name: GetUserByEmail :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email FROM users WHERE lower(email) = lower($1)
|
|
`
|
|
|
|
// Used by forgot-password lookup. Lowercase comparison both sides
|
|
// to keep the unique index happy and to make the lookup
|
|
// case-insensitive.
|
|
func (q *Queries) GetUserByEmail(ctx context.Context, lower string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByEmail, lower)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByID = `-- name: GetUserByID :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email FROM users WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByID, id)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getUserByUsername = `-- name: GetUserByUsername :one
|
|
SELECT id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email FROM users WHERE username = $1
|
|
`
|
|
|
|
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
|
|
row := q.db.QueryRow(ctx, getUserByUsername, username)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listUsers = `-- name: ListUsers :many
|
|
SELECT id, username, display_name, is_admin, auto_approve_requests, created_at
|
|
FROM users
|
|
ORDER BY created_at DESC
|
|
`
|
|
|
|
type ListUsersRow struct {
|
|
ID pgtype.UUID
|
|
Username string
|
|
DisplayName *string
|
|
IsAdmin bool
|
|
AutoApproveRequests bool
|
|
CreatedAt pgtype.Timestamptz
|
|
}
|
|
|
|
// Admin user-management list. Sort newest-first.
|
|
func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) {
|
|
rows, err := q.db.Query(ctx, listUsers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListUsersRow
|
|
for rows.Next() {
|
|
var i ListUsersRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.DisplayName,
|
|
&i.IsAdmin,
|
|
&i.AutoApproveRequests,
|
|
&i.CreatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const regenerateApiToken = `-- name: RegenerateApiToken :one
|
|
UPDATE users SET api_token = $2 WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type RegenerateApiTokenParams struct {
|
|
ID pgtype.UUID
|
|
ApiToken string
|
|
}
|
|
|
|
// Self-service: caller wants a new API token. Used by the /settings
|
|
// API Token card's "Regenerate" button.
|
|
func (q *Queries) RegenerateApiToken(ctx context.Context, arg RegenerateApiTokenParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, regenerateApiToken, arg.ID, arg.ApiToken)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const resetUserPassword = `-- name: ResetUserPassword :exec
|
|
UPDATE users
|
|
SET password_hash = $2
|
|
WHERE id = $1
|
|
`
|
|
|
|
type ResetUserPasswordParams struct {
|
|
ID pgtype.UUID
|
|
PasswordHash string
|
|
}
|
|
|
|
// Admin-driven password reset: caller writes a new hashed password to
|
|
// the target user's row. No knowledge of the old password is required
|
|
// (this is the admin path; self-service "knows current password" is
|
|
// a separate U3 query).
|
|
func (q *Queries) ResetUserPassword(ctx context.Context, arg ResetUserPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, resetUserPassword, arg.ID, arg.PasswordHash)
|
|
return err
|
|
}
|
|
|
|
const setListenBrainzEnabled = `-- name: SetListenBrainzEnabled :exec
|
|
UPDATE users
|
|
SET listenbrainz_enabled = $2
|
|
WHERE id = $1
|
|
`
|
|
|
|
type SetListenBrainzEnabledParams struct {
|
|
ID pgtype.UUID
|
|
ListenbrainzEnabled bool
|
|
}
|
|
|
|
func (q *Queries) SetListenBrainzEnabled(ctx context.Context, arg SetListenBrainzEnabledParams) error {
|
|
_, err := q.db.Exec(ctx, setListenBrainzEnabled, arg.ID, arg.ListenbrainzEnabled)
|
|
return err
|
|
}
|
|
|
|
const setListenBrainzToken = `-- name: SetListenBrainzToken :exec
|
|
UPDATE users
|
|
SET listenbrainz_token = $2,
|
|
listenbrainz_enabled = CASE WHEN COALESCE($2, '') = '' THEN FALSE ELSE listenbrainz_enabled END
|
|
WHERE id = $1
|
|
`
|
|
|
|
type SetListenBrainzTokenParams struct {
|
|
ID pgtype.UUID
|
|
ListenbrainzToken *string
|
|
}
|
|
|
|
func (q *Queries) SetListenBrainzToken(ctx context.Context, arg SetListenBrainzTokenParams) error {
|
|
_, err := q.db.Exec(ctx, setListenBrainzToken, arg.ID, arg.ListenbrainzToken)
|
|
return err
|
|
}
|
|
|
|
const setSubsonicPassword = `-- name: SetSubsonicPassword :exec
|
|
UPDATE users SET subsonic_password = $2 WHERE id = $1
|
|
`
|
|
|
|
type SetSubsonicPasswordParams struct {
|
|
ID pgtype.UUID
|
|
SubsonicPassword *string
|
|
}
|
|
|
|
// Stores (or clears with NULL) the per-user Subsonic legacy credential used
|
|
// for t/s and p auth on /rest/*. Must be plaintext; see migration 0003.
|
|
func (q *Queries) SetSubsonicPassword(ctx context.Context, arg SetSubsonicPasswordParams) error {
|
|
_, err := q.db.Exec(ctx, setSubsonicPassword, arg.ID, arg.SubsonicPassword)
|
|
return err
|
|
}
|
|
|
|
const updateUserAdmin = `-- name: UpdateUserAdmin :one
|
|
UPDATE users
|
|
SET is_admin = $2
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type UpdateUserAdminParams struct {
|
|
ID pgtype.UUID
|
|
IsAdmin bool
|
|
}
|
|
|
|
// Sets is_admin to the given value. Returns the updated row so the
|
|
// handler can echo it back to the caller.
|
|
func (q *Queries) UpdateUserAdmin(ctx context.Context, arg UpdateUserAdminParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, updateUserAdmin, arg.ID, arg.IsAdmin)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserAutoApprove = `-- name: UpdateUserAutoApprove :one
|
|
UPDATE users
|
|
SET auto_approve_requests = $2
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type UpdateUserAutoApproveParams struct {
|
|
ID pgtype.UUID
|
|
AutoApproveRequests bool
|
|
}
|
|
|
|
// Toggle the per-user auto_approve_requests flag.
|
|
func (q *Queries) UpdateUserAutoApprove(ctx context.Context, arg UpdateUserAutoApproveParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, updateUserAutoApprove, arg.ID, arg.AutoApproveRequests)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const updateUserProfile = `-- name: UpdateUserProfile :one
|
|
UPDATE users
|
|
SET display_name = $2,
|
|
email = $3
|
|
WHERE id = $1
|
|
RETURNING id, username, password_hash, api_token, is_admin, created_at, subsonic_password, listenbrainz_token, listenbrainz_enabled, display_name, auto_approve_requests, email
|
|
`
|
|
|
|
type UpdateUserProfileParams struct {
|
|
ID pgtype.UUID
|
|
DisplayName *string
|
|
Email *string
|
|
}
|
|
|
|
// Self-service: set display name and/or email. Both fields are
|
|
// nullable in the DB; pass NULL ptr to clear, non-NULL ptr to set.
|
|
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) (User, error) {
|
|
row := q.db.QueryRow(ctx, updateUserProfile, arg.ID, arg.DisplayName, arg.Email)
|
|
var i User
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Username,
|
|
&i.PasswordHash,
|
|
&i.ApiToken,
|
|
&i.IsAdmin,
|
|
&i.CreatedAt,
|
|
&i.SubsonicPassword,
|
|
&i.ListenbrainzToken,
|
|
&i.ListenbrainzEnabled,
|
|
&i.DisplayName,
|
|
&i.AutoApproveRequests,
|
|
&i.Email,
|
|
)
|
|
return i, err
|
|
}
|