Add user sessions and login

This isn't quite perfect, but gets much of the hard work done.
This commit is contained in:
Eli Ribble 2025-11-05 17:15:33 +00:00
parent e311464b51
commit 486c148bf7
No known key found for this signature in database
28 changed files with 1701 additions and 30 deletions

52
.air.toml Normal file
View file

@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/nidus-sync"
cmd = "go build -o ./tmp/nidus-sync ."
delay = 1000
exclude_dir = ["static", "tmp"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = true
[misc]
clean_on_exit = true
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

157
auth.go Normal file
View file

@ -0,0 +1,157 @@
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"strconv"
"github.com/aarondl/opt/omit"
"github.com/aarondl/opt/omitnull"
"github.com/Gleipnir-Technology/nidus-sync/enums"
"github.com/Gleipnir-Technology/nidus-sync/models"
"github.com/Gleipnir-Technology/nidus-sync/sql"
"golang.org/x/crypto/bcrypt"
)
type NoCredentialsError struct{}
func (e NoCredentialsError) Error() string { return "No credentials were present in the request" }
type InvalidCredentials struct{}
func (e InvalidCredentials) Error() string { return "No username with that password exists" }
type InvalidUsername struct{}
func (e InvalidUsername) Error() string { return "That username doesn't exist" }
func addUserSession(r *http.Request, user *models.User) {
id := strconv.Itoa(int(user.ID))
sessionManager.Put(r.Context(), "user_id", id)
sessionManager.Put(r.Context(), "username", user.Username)
slog.Info("Created new user session",
slog.String("username", user.Username),
slog.String("user_id", id))
}
func getAuthenticatedUser(r *http.Request) (*models.User, error) {
//user_id := sessionManager.GetInt(r.Context(), "user_id")
user_id_str := sessionManager.GetString(r.Context(), "user_id")
user_id, err := strconv.Atoi(user_id_str)
if err != nil {
return nil, fmt.Errorf("Failed to convert user_id to int: %v", err)
}
username := sessionManager.GetString(r.Context(), "username")
slog.Info("Current session info",
slog.Int("user_id", user_id),
slog.String("username", username))
if user_id > 0 && username != "" {
return models.FindUser(r.Context(), PGInstance.BobDB, int32(user_id))
}
// If we can't get the user from the session try to get from auth headers
username, password, ok := r.BasicAuth()
if !ok {
return nil, &NoCredentialsError{}
}
user, err := validateUser(r.Context(), username, password)
if err != nil {
return nil, err
}
addUserSession(r, user)
return user, nil
}
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
func signinUser(r *http.Request, username string, password string) (*models.User, error) {
user, err := validateUser(r.Context(), username, password)
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("No matching user")
}
addUserSession(r, user)
return user, nil
}
func signupUser(username string, name string, password string) (*models.User, error) {
passwordHash, err := hashPassword(password)
if err != nil {
return nil, fmt.Errorf("Cannot signup user: %v", err)
}
setter := models.UserSetter{
DisplayName: omitnull.From(name),
PasswordHash: omitnull.From(passwordHash),
PasswordHashType: omitnull.From(enums.HashtypeBcrypt14),
Username: omit.From(username),
}
u, err := models.Users.Insert(&setter).One(context.TODO(), PGInstance.BobDB)
if err != nil {
return nil, fmt.Errorf("Failed to create user: %v", err)
}
slog.Info("Created user",
slog.Int("ID", int(u.ID)),
slog.String("username", u.Username))
return u, nil
}
func validatePassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func validateUser(ctx context.Context, username string, password string) (*models.User, error) {
passwordHash, err := hashPassword(password)
if err != nil {
return nil, fmt.Errorf("Failed to hash password: %v", err)
}
slog.Info("Validating user",
slog.String("username", username),
slog.String("password", password),
slog.String("hash", passwordHash))
result, err := sql.UserByUsername(username).All(ctx, PGInstance.BobDB)
if err != nil {
return nil, fmt.Errorf("Failed to query for user: %v", err)
}
switch len(result) {
case 0:
return nil, InvalidUsername{}
case 1:
row := result[0]
hash, err := row.PasswordHash.Value()
if err != nil {
return nil, err
}
if hash == nil {
return nil, errors.New("Hash is nil")
}
hashStr, ok := hash.(string);
if !ok {
return nil, errors.New("Hash isn't a string")
}
if !validatePassword(password, hashStr) {
return nil, InvalidCredentials{}
}
user := models.User{
ID: row.ID,
ArcgisAccessToken: row.ArcgisAccessToken,
ArcgisLicense: row.ArcgisLicense,
ArcgisRefreshToken: row.ArcgisRefreshToken,
ArcgisRefreshTokenExpires: row.ArcgisRefreshTokenExpires,
ArcgisRole: row.ArcgisRole,
DisplayName: row.DisplayName,
Email: row.Email,
OrganizationID: row.OrganizationID,
Username: row.Username,
}
return &user, nil
default:
return nil, errors.New("More than one matching row, this should be impossible.")
}
}

View file

@ -24,6 +24,7 @@ var embedMigrations embed.FS
type postgres struct {
BobDB bob.DB
PGXPool *pgxpool.Pool
}
var (
@ -96,7 +97,7 @@ func initializeDatabase(ctx context.Context, uri string) error {
pgOnce.Do(func() {
db, e := pgxpool.New(ctx, uri)
bobDB := bob.NewDB(stdlib.OpenDBFromPool(db))
PGInstance = &postgres{bobDB}
PGInstance = &postgres{bobDB, db}
err = e
})
if err != nil {

17
dberrors/sessions.bob.go Normal file
View file

@ -0,0 +1,17 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dberrors
var SessionErrors = &sessionErrors{
ErrUniqueSessionsPkey: &UniqueConstraintError{
schema: "",
table: "sessions",
columns: []string{"token"},
s: "sessions_pkey",
},
}
type sessionErrors struct {
ErrUniqueSessionsPkey *UniqueConstraintError
}

130
dbinfo/sessions.bob.go Normal file
View file

@ -0,0 +1,130 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package dbinfo
import "github.com/aarondl/opt/null"
var Sessions = Table[
sessionColumns,
sessionIndexes,
sessionForeignKeys,
sessionUniques,
sessionChecks,
]{
Schema: "",
Name: "sessions",
Columns: sessionColumns{
Token: column{
Name: "token",
DBType: "text",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
Data: column{
Name: "data",
DBType: "bytea",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
Expiry: column{
Name: "expiry",
DBType: "timestamp with time zone",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
},
Indexes: sessionIndexes{
SessionsPkey: index{
Type: "btree",
Name: "sessions_pkey",
Columns: []indexColumn{
{
Name: "token",
Desc: null.FromCond(false, true),
IsExpression: false,
},
},
Unique: true,
Comment: "",
NullsFirst: []bool{false},
NullsDistinct: false,
Where: "",
Include: []string{},
},
SessionsExpiryIdx: index{
Type: "btree",
Name: "sessions_expiry_idx",
Columns: []indexColumn{
{
Name: "expiry",
Desc: null.FromCond(false, true),
IsExpression: false,
},
},
Unique: false,
Comment: "",
NullsFirst: []bool{false},
NullsDistinct: false,
Where: "",
Include: []string{},
},
},
PrimaryKey: &constraint{
Name: "sessions_pkey",
Columns: []string{"token"},
Comment: "",
},
Comment: "",
}
type sessionColumns struct {
Token column
Data column
Expiry column
}
func (c sessionColumns) AsSlice() []column {
return []column{
c.Token, c.Data, c.Expiry,
}
}
type sessionIndexes struct {
SessionsPkey index
SessionsExpiryIdx index
}
func (i sessionIndexes) AsSlice() []index {
return []index{
i.SessionsPkey, i.SessionsExpiryIdx,
}
}
type sessionForeignKeys struct{}
func (f sessionForeignKeys) AsSlice() []foreignKey {
return []foreignKey{}
}
type sessionUniques struct{}
func (u sessionUniques) AsSlice() []constraint {
return []constraint{}
}
type sessionChecks struct{}
func (c sessionChecks) AsSlice() []check {
return []check{}
}

View file

@ -1,8 +1,10 @@
package main
import (
"errors"
"log/slog"
"net/http"
"strings"
)
func getFavicon(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "image/x-icon")
@ -11,9 +13,19 @@ func getFavicon(w http.ResponseWriter, r *http.Request) {
}
func getRoot(w http.ResponseWriter, r *http.Request) {
err := htmlRoot(w, r.URL.Path)
user, err := getAuthenticatedUser(r)
if err != nil && !errors.Is(err, &NoCredentialsError{}) {
respondError(w, "Failed to get root", err, http.StatusInternalServerError)
return
}
if user == nil {
errorCode := r.URL.Query().Get("error")
err = htmlSignin(w, errorCode)
} else {
err = htmlDashboard(w, user)
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
respondError(w, "Failed to render root", err, http.StatusInternalServerError)
}
}
func getSignup(w http.ResponseWriter, r *http.Request) {
@ -28,6 +40,33 @@ func respondError(w http.ResponseWriter, m string, e error, s int) {
http.Error(w, m, http.StatusBadRequest)
}
func postSignin(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
respondError(w, "Could not parse form", err, http.StatusBadRequest)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
slog.Info("Signin",
slog.String("username", username),
slog.String("password", strings.Repeat("*", len(password))))
_, err := signinUser(r, username, password)
if err != nil {
if errors.Is(err, InvalidCredentials{}) {
http.Redirect(w, r, "/?error=invalid-credentials", http.StatusFound)
return
}
respondError(w, "Failed to signin user", err, http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/", http.StatusFound)
}
func postSignup(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
respondError(w, "Could not parse form", err, http.StatusBadRequest)
@ -41,7 +80,8 @@ func postSignup(w http.ResponseWriter, r *http.Request) {
slog.Info("Signup",
slog.String("username", username),
slog.String("name", name))
slog.String("name", name),
slog.String("password", strings.Repeat("*", len(password))))
if terms != "on" {
slog.Error("Terms not agreed", slog.String("terms", terms))
@ -49,10 +89,13 @@ func postSignup(w http.ResponseWriter, r *http.Request) {
return
}
if err := signupUser(username, name, password); err != nil {
user, err := signupUser(username, name, password)
if err != nil {
respondError(w, "Failed to signup user", err, http.StatusInternalServerError)
return
}
addUserSession(r, user)
http.Redirect(w, r, "/", http.StatusFound)
}

View file

@ -15,6 +15,9 @@ var (
organizationWithParentsCascadingCtx = newContextual[bool]("organizationWithParentsCascading")
organizationRelUserCtx = newContextual[bool]("organization.user_.user_.user__organization_id_fkey")
// Relationship Contexts for sessions
sessionWithParentsCascadingCtx = newContextual[bool]("sessionWithParentsCascading")
// Relationship Contexts for user_
userWithParentsCascadingCtx = newContextual[bool]("userWithParentsCascading")
userRelOrganizationCtx = newContextual[bool]("organization.user_.user_.user__organization_id_fkey")

View file

@ -15,6 +15,7 @@ import (
type Factory struct {
baseGooseDBVersionMods GooseDBVersionModSlice
baseOrganizationMods OrganizationModSlice
baseSessionMods SessionModSlice
baseUserMods UserModSlice
}
@ -79,6 +80,32 @@ func (f *Factory) FromExistingOrganization(m *models.Organization) *Organization
return o
}
func (f *Factory) NewSession(mods ...SessionMod) *SessionTemplate {
return f.NewSessionWithContext(context.Background(), mods...)
}
func (f *Factory) NewSessionWithContext(ctx context.Context, mods ...SessionMod) *SessionTemplate {
o := &SessionTemplate{f: f}
if f != nil {
f.baseSessionMods.Apply(ctx, o)
}
SessionModSlice(mods).Apply(ctx, o)
return o
}
func (f *Factory) FromExistingSession(m *models.Session) *SessionTemplate {
o := &SessionTemplate{f: f, alreadyPersisted: true}
o.Token = func() string { return m.Token }
o.Data = func() []byte { return m.Data }
o.Expiry = func() time.Time { return m.Expiry }
return o
}
func (f *Factory) NewUser(mods ...UserMod) *UserTemplate {
return f.NewUserWithContext(context.Background(), mods...)
}
@ -135,6 +162,14 @@ func (f *Factory) AddBaseOrganizationMod(mods ...OrganizationMod) {
f.baseOrganizationMods = append(f.baseOrganizationMods, mods...)
}
func (f *Factory) ClearBaseSessionMods() {
f.baseSessionMods = nil
}
func (f *Factory) AddBaseSessionMod(mods ...SessionMod) {
f.baseSessionMods = append(f.baseSessionMods, mods...)
}
func (f *Factory) ClearBaseUserMods() {
f.baseUserMods = nil
}

View file

@ -56,6 +56,30 @@ func TestCreateOrganization(t *testing.T) {
}
}
func TestCreateSession(t *testing.T) {
if testDB == nil {
t.Skip("skipping test, no DSN provided")
}
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
tx, err := testDB.Begin(ctx)
if err != nil {
t.Fatalf("Error starting transaction: %v", err)
}
defer func() {
if err := tx.Rollback(ctx); err != nil {
t.Fatalf("Error rolling back transaction: %v", err)
}
}()
if _, err := New().NewSessionWithContext(ctx).Create(ctx, tx); err != nil {
t.Fatalf("Error creating Session: %v", err)
}
}
func TestCreateUser(t *testing.T) {
if testDB == nil {
t.Skip("skipping test, no DSN provided")

View file

@ -14,6 +14,14 @@ import (
var defaultFaker = faker.New()
func random___byte(f *faker.Faker, limits ...string) []byte {
if f == nil {
f = &defaultFaker
}
return []byte(random_string(f, limits...))
}
func random_bool(f *faker.Faker, limits ...string) bool {
if f == nil {
f = &defaultFaker

View file

@ -4,6 +4,7 @@
package factory
import (
"bytes"
"testing"
"github.com/stephenafamo/bob"
@ -12,6 +13,17 @@ import (
// Set the testDB to enable tests that use the database
var testDB bob.Transactor[bob.Tx]
func TestRandom___byte(t *testing.T) {
t.Parallel()
val1 := random___byte(nil)
val2 := random___byte(nil)
if bytes.Equal(val1, val2) {
t.Fatalf("random___byte() returned the same value twice: %v", val1)
}
}
func TestRandom_int32(t *testing.T) {
t.Parallel()

344
factory/sessions.bob.go Normal file
View file

@ -0,0 +1,344 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package factory
import (
"context"
"testing"
"time"
models "github.com/Gleipnir-Technology/nidus-sync/models"
"github.com/aarondl/opt/omit"
"github.com/jaswdr/faker/v2"
"github.com/stephenafamo/bob"
)
type SessionMod interface {
Apply(context.Context, *SessionTemplate)
}
type SessionModFunc func(context.Context, *SessionTemplate)
func (f SessionModFunc) Apply(ctx context.Context, n *SessionTemplate) {
f(ctx, n)
}
type SessionModSlice []SessionMod
func (mods SessionModSlice) Apply(ctx context.Context, n *SessionTemplate) {
for _, f := range mods {
f.Apply(ctx, n)
}
}
// SessionTemplate is an object representing the database table.
// all columns are optional and should be set by mods
type SessionTemplate struct {
Token func() string
Data func() []byte
Expiry func() time.Time
f *Factory
alreadyPersisted bool
}
// Apply mods to the SessionTemplate
func (o *SessionTemplate) Apply(ctx context.Context, mods ...SessionMod) {
for _, mod := range mods {
mod.Apply(ctx, o)
}
}
// setModelRels creates and sets the relationships on *models.Session
// according to the relationships in the template. Nothing is inserted into the db
func (t SessionTemplate) setModelRels(o *models.Session) {}
// BuildSetter returns an *models.SessionSetter
// this does nothing with the relationship templates
func (o SessionTemplate) BuildSetter() *models.SessionSetter {
m := &models.SessionSetter{}
if o.Token != nil {
val := o.Token()
m.Token = omit.From(val)
}
if o.Data != nil {
val := o.Data()
m.Data = omit.From(val)
}
if o.Expiry != nil {
val := o.Expiry()
m.Expiry = omit.From(val)
}
return m
}
// BuildManySetter returns an []*models.SessionSetter
// this does nothing with the relationship templates
func (o SessionTemplate) BuildManySetter(number int) []*models.SessionSetter {
m := make([]*models.SessionSetter, number)
for i := range m {
m[i] = o.BuildSetter()
}
return m
}
// Build returns an *models.Session
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use SessionTemplate.Create
func (o SessionTemplate) Build() *models.Session {
m := &models.Session{}
if o.Token != nil {
m.Token = o.Token()
}
if o.Data != nil {
m.Data = o.Data()
}
if o.Expiry != nil {
m.Expiry = o.Expiry()
}
o.setModelRels(m)
return m
}
// BuildMany returns an models.SessionSlice
// Related objects are also created and placed in the .R field
// NOTE: Objects are not inserted into the database. Use SessionTemplate.CreateMany
func (o SessionTemplate) BuildMany(number int) models.SessionSlice {
m := make(models.SessionSlice, number)
for i := range m {
m[i] = o.Build()
}
return m
}
func ensureCreatableSession(m *models.SessionSetter) {
if !(m.Token.IsValue()) {
val := random_string(nil)
m.Token = omit.From(val)
}
if !(m.Data.IsValue()) {
val := random___byte(nil)
m.Data = omit.From(val)
}
if !(m.Expiry.IsValue()) {
val := random_time_Time(nil)
m.Expiry = omit.From(val)
}
}
// insertOptRels creates and inserts any optional the relationships on *models.Session
// according to the relationships in the template.
// any required relationship should have already exist on the model
func (o *SessionTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.Session) error {
var err error
return err
}
// Create builds a session and inserts it into the database
// Relations objects are also inserted and placed in the .R field
func (o *SessionTemplate) Create(ctx context.Context, exec bob.Executor) (*models.Session, error) {
var err error
opt := o.BuildSetter()
ensureCreatableSession(opt)
m, err := models.Sessions.Insert(opt).One(ctx, exec)
if err != nil {
return nil, err
}
if err := o.insertOptRels(ctx, exec, m); err != nil {
return nil, err
}
return m, err
}
// MustCreate builds a session and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o *SessionTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.Session {
m, err := o.Create(ctx, exec)
if err != nil {
panic(err)
}
return m
}
// CreateOrFail builds a session and inserts it into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o *SessionTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.Session {
tb.Helper()
m, err := o.Create(ctx, exec)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// CreateMany builds multiple sessions and inserts them into the database
// Relations objects are also inserted and placed in the .R field
func (o SessionTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.SessionSlice, error) {
var err error
m := make(models.SessionSlice, number)
for i := range m {
m[i], err = o.Create(ctx, exec)
if err != nil {
return nil, err
}
}
return m, nil
}
// MustCreateMany builds multiple sessions and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// panics if an error occurs
func (o SessionTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.SessionSlice {
m, err := o.CreateMany(ctx, exec, number)
if err != nil {
panic(err)
}
return m
}
// CreateManyOrFail builds multiple sessions and inserts them into the database
// Relations objects are also inserted and placed in the .R field
// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs
func (o SessionTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.SessionSlice {
tb.Helper()
m, err := o.CreateMany(ctx, exec, number)
if err != nil {
tb.Fatal(err)
return nil
}
return m
}
// Session has methods that act as mods for the SessionTemplate
var SessionMods sessionMods
type sessionMods struct{}
func (m sessionMods) RandomizeAllColumns(f *faker.Faker) SessionMod {
return SessionModSlice{
SessionMods.RandomToken(f),
SessionMods.RandomData(f),
SessionMods.RandomExpiry(f),
}
}
// Set the model columns to this value
func (m sessionMods) Token(val string) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Token = func() string { return val }
})
}
// Set the Column from the function
func (m sessionMods) TokenFunc(f func() string) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Token = f
})
}
// Clear any values for the column
func (m sessionMods) UnsetToken() SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Token = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m sessionMods) RandomToken(f *faker.Faker) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Token = func() string {
return random_string(f)
}
})
}
// Set the model columns to this value
func (m sessionMods) Data(val []byte) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Data = func() []byte { return val }
})
}
// Set the Column from the function
func (m sessionMods) DataFunc(f func() []byte) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Data = f
})
}
// Clear any values for the column
func (m sessionMods) UnsetData() SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Data = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m sessionMods) RandomData(f *faker.Faker) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Data = func() []byte {
return random___byte(f)
}
})
}
// Set the model columns to this value
func (m sessionMods) Expiry(val time.Time) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Expiry = func() time.Time { return val }
})
}
// Set the Column from the function
func (m sessionMods) ExpiryFunc(f func() time.Time) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Expiry = f
})
}
// Clear any values for the column
func (m sessionMods) UnsetExpiry() SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Expiry = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m sessionMods) RandomExpiry(f *faker.Faker) SessionMod {
return SessionModFunc(func(_ context.Context, o *SessionTemplate) {
o.Expiry = func() time.Time {
return random_time_Time(f)
}
})
}
func (m sessionMods) WithParentsCascading() SessionMod {
return SessionModFunc(func(ctx context.Context, o *SessionTemplate) {
if isDone, _ := sessionWithParentsCascadingCtx.Value(ctx); isDone {
return
}
ctx = sessionWithParentsCascadingCtx.WithValue(ctx, true)
})
}

View file

@ -19,6 +19,7 @@
# Development shell configuration
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.air
pkgs.go
pkgs.goose
pkgs.gotools

3
go.mod
View file

@ -4,6 +4,7 @@ go 1.24.9
require (
github.com/aarondl/opt v0.0.0-20250607033636-982744e1bd65
github.com/alexedwards/scs/pgxstore v0.0.0-20251002162104-209de6e426de
github.com/alexedwards/scs/v2 v2.9.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-webauthn/webauthn v0.14.0
@ -14,6 +15,7 @@ require (
github.com/pressly/goose/v3 v3.26.0
github.com/stephenafamo/bob v0.41.1
github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07
golang.org/x/crypto v0.42.0
)
require (
@ -34,7 +36,6 @@ require (
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect

59
go.sum
View file

@ -6,6 +6,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aarondl/opt v0.0.0-20250607033636-982744e1bd65 h1:lbdPe4LBNmNDzeQFwNhEc88w90841qv737MI4+aXSYU=
github.com/aarondl/opt v0.0.0-20250607033636-982744e1bd65/go.mod h1:+xKBXrTAUOvrDXO5PRwIr4E1wciHY3Glgl+6OkCXknU=
github.com/alexedwards/scs/pgxstore v0.0.0-20251002162104-209de6e426de h1:wNJVpr0ag/BL2nRGBIESdLe1qoljXIolF/qPi1gleRA=
github.com/alexedwards/scs/pgxstore v0.0.0-20251002162104-209de6e426de/go.mod h1:hwveArYcjyOK66EViVgVU5Iqj7zyEsWjKXMQhDJrTLI=
github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90=
github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
@ -20,6 +22,7 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -65,16 +68,25 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaswdr/faker/v2 v2.8.1 h1:2AcPgHDBXYQregFUH9LgVZKfFupc4SIquYhp29sf5wQ=
github.com/jaswdr/faker/v2 v2.8.1/go.mod h1:jZq+qzNQr8/P+5fHd9t3txe2GNPnthrTfohtnJ7B+68=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
@ -123,6 +135,7 @@ github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmY
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
@ -136,9 +149,14 @@ github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97/go.mod h1:bM3V
github.com/stephenafamo/scan v0.7.0 h1:lfFiD9H5+n4AdK3qNzXQjj2M3NfTOpmWBIA39NwB94c=
github.com/stephenafamo/scan v0.7.0/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
@ -157,6 +175,7 @@ github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8S
github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
@ -173,22 +192,62 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

38
html.go
View file

@ -2,15 +2,17 @@ package main
import (
"errors"
"fmt"
"html/template"
"io"
"log"
"os"
"github.com/Gleipnir-Technology/nidus-sync/models"
)
var (
dashboard = newBuiltTemplate("dashboard", "base")
root = newBuiltTemplate("root", "base")
signin = newBuiltTemplate("signin", "base")
signup = newBuiltTemplate("signup", "base")
)
@ -27,18 +29,20 @@ type ContentDashboard struct {
BabbleLinks []Link
Username string
}
type ContentRoot struct {
BabbleLinks []Link
}
type ContentSignup struct {
type ContentSignin struct {
InvalidCredentials bool
}
type ContentSignup struct { }
func (bt *BuiltTemplate) ExecuteTemplate(w io.Writer, data any) error {
name := bt.files[0] + ".html"
if bt.template == nil {
templ := parseFromDisk(bt.files)
templ, err := parseFromDisk(bt.files)
if err != nil {
return fmt.Errorf("Failed to parse template file: %v", err)
}
if templ == nil {
w.Write([]byte("Failed to read from disk"))
w.Write([]byte("Failed to read from disk: "))
return errors.New("Template parsing failed")
}
return templ.ExecuteTemplate(w, name, data)
@ -47,17 +51,18 @@ func (bt *BuiltTemplate) ExecuteTemplate(w io.Writer, data any) error {
}
}
func htmlDashboard(w io.Writer, path string, username string) error {
func htmlDashboard(w io.Writer, user *models.User) error {
data := ContentDashboard{
Username: username,
Username: user.Username,
}
return dashboard.ExecuteTemplate(w, data)
}
func htmlRoot(w io.Writer, path string) error {
data := ContentRoot{
func htmlSignin(w io.Writer, errorCode string) error {
data := ContentSignin{
InvalidCredentials: errorCode == "invalid-credentials",
}
return root.ExecuteTemplate(w, data)
return signin.ExecuteTemplate(w, data)
}
func htmlSignup(w io.Writer, path string) error {
@ -96,7 +101,7 @@ func parseEmbedded(files []string) *template.Template {
return nil
}
func parseFromDisk(files []string) *template.Template {
func parseFromDisk(files []string) (*template.Template, error) {
funcMap := makeFuncMap()
paths := make([]string, 0)
for _, f := range files {
@ -105,8 +110,7 @@ func parseFromDisk(files []string) *template.Template {
name := files[0] + ".html"
templ, err := template.New(name).Funcs(funcMap).ParseFiles(paths...)
if err != nil {
log.Println("TEMPLATE FAILED", err)
return nil
return nil, fmt.Errorf("Failed to parse %s: %v", paths, err)
}
return templ
return templ, nil
}

View file

@ -7,6 +7,7 @@ import (
"os"
"time"
"github.com/alexedwards/scs/pgxstore"
"github.com/alexedwards/scs/v2"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@ -49,6 +50,7 @@ func main() {
os.Exit(2)
}
sessionManager = scs.New()
sessionManager.Store = pgxstore.New(PGInstance.PGXPool)
sessionManager.Lifetime = 24 * time.Hour
r := chi.NewRouter()
@ -56,6 +58,7 @@ func main() {
r.Use(sessionManager.LoadAndSave)
r.Get("/", getRoot)
r.Post("/signin", postSignin)
r.Get("/signup", getSignup)
r.Post("/signup", postSignup)
r.Get("/favicon.ico", getFavicon)

View file

@ -0,0 +1,12 @@
-- +goose Up
CREATE TABLE sessions (
token TEXT PRIMARY KEY,
data BYTEA NOT NULL,
expiry TIMESTAMPTZ NOT NULL
);
CREATE INDEX sessions_expiry_idx ON sessions (expiry);
-- +goose Down
DROP TABLE sessions;

View file

@ -20,6 +20,9 @@ var _ bob.HookableType = &GooseDBVersion{}
// Make sure the type Organization runs hooks after queries
var _ bob.HookableType = &Organization{}
// Make sure the type Session runs hooks after queries
var _ bob.HookableType = &Session{}
// Make sure the type User runs hooks after queries
var _ bob.HookableType = &User{}

View file

@ -19,15 +19,18 @@ var (
func Where[Q psql.Filterable]() struct {
GooseDBVersions gooseDBVersionWhere[Q]
Organizations organizationWhere[Q]
Sessions sessionWhere[Q]
Users userWhere[Q]
} {
return struct {
GooseDBVersions gooseDBVersionWhere[Q]
Organizations organizationWhere[Q]
Sessions sessionWhere[Q]
Users userWhere[Q]
}{
GooseDBVersions: buildGooseDBVersionWhere[Q](GooseDBVersions.Columns),
Organizations: buildOrganizationWhere[Q](Organizations.Columns),
Sessions: buildSessionWhere[Q](Sessions.Columns),
Users: buildUserWhere[Q](Users.Columns),
}
}

399
models/sessions.bob.go Normal file
View file

@ -0,0 +1,399 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package models
import (
"context"
"io"
"time"
"github.com/aarondl/opt/omit"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql"
"github.com/stephenafamo/bob/dialect/psql/dialect"
"github.com/stephenafamo/bob/dialect/psql/dm"
"github.com/stephenafamo/bob/dialect/psql/sm"
"github.com/stephenafamo/bob/dialect/psql/um"
"github.com/stephenafamo/bob/expr"
)
// Session is an object representing the database table.
type Session struct {
Token string `db:"token,pk" `
Data []byte `db:"data" `
Expiry time.Time `db:"expiry" `
}
// SessionSlice is an alias for a slice of pointers to Session.
// This should almost always be used instead of []*Session.
type SessionSlice []*Session
// Sessions contains methods to work with the sessions table
var Sessions = psql.NewTablex[*Session, SessionSlice, *SessionSetter]("", "sessions", buildSessionColumns("sessions"))
// SessionsQuery is a query on the sessions table
type SessionsQuery = *psql.ViewQuery[*Session, SessionSlice]
func buildSessionColumns(alias string) sessionColumns {
return sessionColumns{
ColumnsExpr: expr.NewColumnsExpr(
"token", "data", "expiry",
).WithParent("sessions"),
tableAlias: alias,
Token: psql.Quote(alias, "token"),
Data: psql.Quote(alias, "data"),
Expiry: psql.Quote(alias, "expiry"),
}
}
type sessionColumns struct {
expr.ColumnsExpr
tableAlias string
Token psql.Expression
Data psql.Expression
Expiry psql.Expression
}
func (c sessionColumns) Alias() string {
return c.tableAlias
}
func (sessionColumns) AliasedAs(alias string) sessionColumns {
return buildSessionColumns(alias)
}
// SessionSetter is used for insert/upsert/update operations
// All values are optional, and do not have to be set
// Generated columns are not included
type SessionSetter struct {
Token omit.Val[string] `db:"token,pk" `
Data omit.Val[[]byte] `db:"data" `
Expiry omit.Val[time.Time] `db:"expiry" `
}
func (s SessionSetter) SetColumns() []string {
vals := make([]string, 0, 3)
if s.Token.IsValue() {
vals = append(vals, "token")
}
if s.Data.IsValue() {
vals = append(vals, "data")
}
if s.Expiry.IsValue() {
vals = append(vals, "expiry")
}
return vals
}
func (s SessionSetter) Overwrite(t *Session) {
if s.Token.IsValue() {
t.Token = s.Token.MustGet()
}
if s.Data.IsValue() {
t.Data = s.Data.MustGet()
}
if s.Expiry.IsValue() {
t.Expiry = s.Expiry.MustGet()
}
}
func (s *SessionSetter) Apply(q *dialect.InsertQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Sessions.BeforeInsertHooks.RunHooks(ctx, exec, s)
})
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 3)
if s.Token.IsValue() {
vals[0] = psql.Arg(s.Token.MustGet())
} else {
vals[0] = psql.Raw("DEFAULT")
}
if s.Data.IsValue() {
vals[1] = psql.Arg(s.Data.MustGet())
} else {
vals[1] = psql.Raw("DEFAULT")
}
if s.Expiry.IsValue() {
vals[2] = psql.Arg(s.Expiry.MustGet())
} else {
vals[2] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
}))
}
func (s SessionSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return um.Set(s.Expressions()...)
}
func (s SessionSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 3)
if s.Token.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "token")...),
psql.Arg(s.Token),
}})
}
if s.Data.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "data")...),
psql.Arg(s.Data),
}})
}
if s.Expiry.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "expiry")...),
psql.Arg(s.Expiry),
}})
}
return exprs
}
// FindSession retrieves a single record by primary key
// If cols is empty Find will return all columns.
func FindSession(ctx context.Context, exec bob.Executor, TokenPK string, cols ...string) (*Session, error) {
if len(cols) == 0 {
return Sessions.Query(
sm.Where(Sessions.Columns.Token.EQ(psql.Arg(TokenPK))),
).One(ctx, exec)
}
return Sessions.Query(
sm.Where(Sessions.Columns.Token.EQ(psql.Arg(TokenPK))),
sm.Columns(Sessions.Columns.Only(cols...)),
).One(ctx, exec)
}
// SessionExists checks the presence of a single record by primary key
func SessionExists(ctx context.Context, exec bob.Executor, TokenPK string) (bool, error) {
return Sessions.Query(
sm.Where(Sessions.Columns.Token.EQ(psql.Arg(TokenPK))),
).Exists(ctx, exec)
}
// AfterQueryHook is called after Session is retrieved from the database
func (o *Session) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Sessions.AfterSelectHooks.RunHooks(ctx, exec, SessionSlice{o})
case bob.QueryTypeInsert:
ctx, err = Sessions.AfterInsertHooks.RunHooks(ctx, exec, SessionSlice{o})
case bob.QueryTypeUpdate:
ctx, err = Sessions.AfterUpdateHooks.RunHooks(ctx, exec, SessionSlice{o})
case bob.QueryTypeDelete:
ctx, err = Sessions.AfterDeleteHooks.RunHooks(ctx, exec, SessionSlice{o})
}
return err
}
// primaryKeyVals returns the primary key values of the Session
func (o *Session) primaryKeyVals() bob.Expression {
return psql.Arg(o.Token)
}
func (o *Session) pkEQ() dialect.Expression {
return psql.Quote("sessions", "token").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
return o.primaryKeyVals().WriteSQL(ctx, w, d, start)
}))
}
// Update uses an executor to update the Session
func (o *Session) Update(ctx context.Context, exec bob.Executor, s *SessionSetter) error {
v, err := Sessions.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec)
if err != nil {
return err
}
*o = *v
return nil
}
// Delete deletes a single Session record with an executor
func (o *Session) Delete(ctx context.Context, exec bob.Executor) error {
_, err := Sessions.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec)
return err
}
// Reload refreshes the Session using the executor
func (o *Session) Reload(ctx context.Context, exec bob.Executor) error {
o2, err := Sessions.Query(
sm.Where(Sessions.Columns.Token.EQ(psql.Arg(o.Token))),
).One(ctx, exec)
if err != nil {
return err
}
*o = *o2
return nil
}
// AfterQueryHook is called after SessionSlice is retrieved from the database
func (o SessionSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
var err error
switch queryType {
case bob.QueryTypeSelect:
ctx, err = Sessions.AfterSelectHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeInsert:
ctx, err = Sessions.AfterInsertHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeUpdate:
ctx, err = Sessions.AfterUpdateHooks.RunHooks(ctx, exec, o)
case bob.QueryTypeDelete:
ctx, err = Sessions.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}
func (o SessionSlice) pkIN() dialect.Expression {
if len(o) == 0 {
return psql.Raw("NULL")
}
return psql.Quote("sessions", "token").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
pkPairs := make([]bob.Expression, len(o))
for i, row := range o {
pkPairs[i] = row.primaryKeyVals()
}
return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "")
}))
}
// copyMatchingRows finds models in the given slice that have the same primary key
// then it first copies the existing relationships from the old model to the new model
// and then replaces the old model in the slice with the new model
func (o SessionSlice) copyMatchingRows(from ...*Session) {
for i, old := range o {
for _, new := range from {
if new.Token != old.Token {
continue
}
o[i] = new
break
}
}
}
// UpdateMod modifies an update query with "WHERE primary_key IN (o...)"
func (o SessionSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Sessions.BeforeUpdateHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *Session:
o.copyMatchingRows(retrieved)
case []*Session:
o.copyMatchingRows(retrieved...)
case SessionSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a Session or a slice of Session
// then run the AfterUpdateHooks on the slice
_, err = Sessions.AfterUpdateHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)"
func (o SessionSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] {
return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) {
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
return Sessions.BeforeDeleteHooks.RunHooks(ctx, exec, o)
})
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
var err error
switch retrieved := retrieved.(type) {
case *Session:
o.copyMatchingRows(retrieved)
case []*Session:
o.copyMatchingRows(retrieved...)
case SessionSlice:
o.copyMatchingRows(retrieved...)
default:
// If the retrieved value is not a Session or a slice of Session
// then run the AfterDeleteHooks on the slice
_, err = Sessions.AfterDeleteHooks.RunHooks(ctx, exec, o)
}
return err
}))
q.AppendWhere(o.pkIN())
})
}
func (o SessionSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals SessionSetter) error {
if len(o) == 0 {
return nil
}
_, err := Sessions.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec)
return err
}
func (o SessionSlice) DeleteAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
_, err := Sessions.Delete(o.DeleteMod()).Exec(ctx, exec)
return err
}
func (o SessionSlice) ReloadAll(ctx context.Context, exec bob.Executor) error {
if len(o) == 0 {
return nil
}
o2, err := Sessions.Query(sm.Where(o.pkIN())).All(ctx, exec)
if err != nil {
return err
}
o.copyMatchingRows(o2...)
return nil
}
type sessionWhere[Q psql.Filterable] struct {
Token psql.WhereMod[Q, string]
Data psql.WhereMod[Q, []byte]
Expiry psql.WhereMod[Q, time.Time]
}
func (sessionWhere[Q]) AliasedAs(alias string) sessionWhere[Q] {
return buildSessionWhere[Q](buildSessionColumns(alias))
}
func buildSessionWhere[Q psql.Filterable](cols sessionColumns) sessionWhere[Q] {
return sessionWhere[Q]{
Token: psql.Where[Q, string](cols.Token),
Data: psql.Where[Q, []byte](cols.Data),
Expiry: psql.Where[Q, time.Time](cols.Expiry),
}
}

View file

@ -0,0 +1,84 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package sql
import (
"strconv"
"strings"
"time"
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
"github.com/jaswdr/faker/v2"
"github.com/stephenafamo/bob"
pg_query "github.com/wasilibs/go-pgquery"
)
// Set the testDB to enable tests that use the database
var testDB bob.Transactor[bob.Tx]
func formatQuery(s string) (string, error) {
aTree, err := pg_query.Parse(s)
if err != nil {
return "", err
}
return pg_query.Deparse(aTree)
}
var defaultFaker = faker.New()
func random_enums_ArcgisLicenseType(f *faker.Faker, limits ...string) enums.ArcgisLicenseType {
if f == nil {
f = &defaultFaker
}
var e enums.ArcgisLicenseType
all := e.All()
return all[f.IntBetween(0, len(all)-1)]
}
func random_enums_Hashtype(f *faker.Faker, limits ...string) enums.Hashtype {
if f == nil {
f = &defaultFaker
}
var e enums.Hashtype
all := e.All()
return all[f.IntBetween(0, len(all)-1)]
}
func random_int32(f *faker.Faker, limits ...string) int32 {
if f == nil {
f = &defaultFaker
}
return f.Int32()
}
func random_string(f *faker.Faker, limits ...string) string {
if f == nil {
f = &defaultFaker
}
val := strings.Join(f.Lorem().Words(f.IntBetween(1, 5)), " ")
if len(limits) == 0 {
return val
}
limitInt, _ := strconv.Atoi(limits[0])
if limitInt > 0 && limitInt < len(val) {
val = val[:limitInt]
}
return val
}
func random_time_Time(f *faker.Faker, limits ...string) time.Time {
if f == nil {
f = &defaultFaker
}
year := time.Hour * 24 * 365
min := time.Now().Add(-year)
max := time.Now().Add(year)
return f.Time().TimeBetween(min, max)
}

116
sql/user_by_username.bob.go Normal file
View file

@ -0,0 +1,116 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package sql
import (
"context"
_ "embed"
"io"
"iter"
"time"
enums "github.com/Gleipnir-Technology/nidus-sync/enums"
"github.com/aarondl/opt/null"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql"
"github.com/stephenafamo/bob/dialect/psql/dialect"
"github.com/stephenafamo/bob/orm"
"github.com/stephenafamo/scan"
)
//go:embed user_by_username.bob.sql
var formattedQueries_user_by_username string
var userByUsernameSQL = formattedQueries_user_by_username[152:773]
type UserByUsernameQuery = orm.ModQuery[*dialect.SelectQuery, userByUsername, UserByUsernameRow, []UserByUsernameRow, userByUsernameTransformer]
func UserByUsername(Username string) *UserByUsernameQuery {
var expressionTypArgs userByUsername
expressionTypArgs.Username = psql.Arg(Username)
return &UserByUsernameQuery{
Query: orm.Query[userByUsername, UserByUsernameRow, []UserByUsernameRow, userByUsernameTransformer]{
ExecQuery: orm.ExecQuery[userByUsername]{
BaseQuery: bob.BaseQuery[userByUsername]{
Expression: expressionTypArgs,
Dialect: dialect.Dialect,
QueryType: bob.QueryTypeSelect,
},
},
Scanner: func(context.Context, []string) (func(*scan.Row) (any, error), func(any) (UserByUsernameRow, error)) {
return func(row *scan.Row) (any, error) {
var t UserByUsernameRow
row.ScheduleScanByIndex(0, &t.ID)
row.ScheduleScanByIndex(1, &t.ArcgisAccessToken)
row.ScheduleScanByIndex(2, &t.ArcgisLicense)
row.ScheduleScanByIndex(3, &t.ArcgisRefreshToken)
row.ScheduleScanByIndex(4, &t.ArcgisRefreshTokenExpires)
row.ScheduleScanByIndex(5, &t.ArcgisRole)
row.ScheduleScanByIndex(6, &t.DisplayName)
row.ScheduleScanByIndex(7, &t.Email)
row.ScheduleScanByIndex(8, &t.OrganizationID)
row.ScheduleScanByIndex(9, &t.Username)
row.ScheduleScanByIndex(10, &t.PasswordHashType)
row.ScheduleScanByIndex(11, &t.PasswordHash)
return &t, nil
}, func(v any) (UserByUsernameRow, error) {
return *(v.(*UserByUsernameRow)), nil
}
},
},
Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) {
q.AppendSelect(expressionTypArgs.subExpr(7, 551))
q.SetTable(expressionTypArgs.subExpr(557, 562))
q.AppendWhere(expressionTypArgs.subExpr(570, 621))
}),
}
}
type UserByUsernameRow = struct {
ID int32 `db:"id"`
ArcgisAccessToken null.Val[string] `db:"arcgis_access_token"`
ArcgisLicense null.Val[enums.ArcgisLicenseType] `db:"arcgis_license"`
ArcgisRefreshToken null.Val[string] `db:"arcgis_refresh_token"`
ArcgisRefreshTokenExpires null.Val[time.Time] `db:"arcgis_refresh_token_expires"`
ArcgisRole null.Val[string] `db:"arcgis_role"`
DisplayName null.Val[string] `db:"display_name"`
Email null.Val[string] `db:"email"`
OrganizationID null.Val[int32] `db:"organization_id"`
Username string `db:"username"`
PasswordHashType null.Val[enums.Hashtype] `db:"password_hash_type"`
PasswordHash null.Val[string] `db:"password_hash"`
}
type userByUsernameTransformer = bob.SliceTransformer[UserByUsernameRow, []UserByUsernameRow]
type userByUsername struct {
Username bob.Expression
}
func (o userByUsername) args() iter.Seq[orm.ArgWithPosition] {
return func(yield func(arg orm.ArgWithPosition) bool) {
if !yield(orm.ArgWithPosition{
Name: "username",
Start: 581,
Stop: 583,
Expression: o.Username,
}) {
return
}
}
}
func (o userByUsername) raw(from, to int) string {
return userByUsernameSQL[from:to]
}
func (o userByUsername) subExpr(from, to int) bob.Expression {
return orm.ArgsToExpression(userByUsernameSQL, from, to, o.args())
}
func (o userByUsername) WriteSQL(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
return o.subExpr(0, len(userByUsernameSQL)).WriteSQL(ctx, w, d, start)
}

View file

@ -0,0 +1,7 @@
-- Code generated by BobGen psql v0.41.1. DO NOT EDIT.
-- This file is meant to be re-generated in place and/or deleted at any time.
-- UserByUsername
SELECT "user_"."id" AS "id", "user_"."arcgis_access_token" AS "arcgis_access_token", "user_"."arcgis_license" AS "arcgis_license", "user_"."arcgis_refresh_token" AS "arcgis_refresh_token", "user_"."arcgis_refresh_token_expires" AS "arcgis_refresh_token_expires", "user_"."arcgis_role" AS "arcgis_role", "user_"."display_name" AS "display_name", "user_"."email" AS "email", "user_"."organization_id" AS "organization_id", "user_"."username" AS "username", "user_"."password_hash_type" AS "password_hash_type", "user_"."password_hash" AS "password_hash" FROM user_ WHERE
username = $1 AND
password_hash_type = 'bcrypt-14';

View file

@ -0,0 +1,139 @@
// Code generated by BobGen psql v0.41.1. DO NOT EDIT.
// This file is meant to be re-generated in place and/or deleted at any time.
package sql
import (
"context"
"fmt"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stephenafamo/bob"
"github.com/stephenafamo/bob/dialect/psql"
testutils "github.com/stephenafamo/bob/test/utils"
)
func TestUserByUsername(t *testing.T) {
t.Run("Base", func(t *testing.T) {
var sb strings.Builder
query := UserByUsername(random_string(nil))
if _, err := query.WriteQuery(t.Context(), &sb, 1); err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(userByUsernameSQL, sb.String()); diff != "" {
t.Fatalf("unexpected result (-got +want):\n%s", diff)
}
})
t.Run("Mod", func(t *testing.T) {
var sb strings.Builder
query := UserByUsername(random_string(nil))
if _, err := psql.Select(query).WriteQuery(t.Context(), &sb, 1); err != nil {
t.Fatal(err)
}
queryDiff, err := testutils.QueryDiff(userByUsernameSQL, sb.String(), formatQuery)
if err != nil {
t.Fatal(err)
}
if queryDiff != "" {
fmt.Println(sb.String())
t.Fatalf("unexpected result (-got +want):\n%s", queryDiff)
}
})
t.Run("Scanning", func(t *testing.T) {
if testDB == nil {
t.Skip("skipping test, no DSN provided")
}
ctxTx, cancel := context.WithCancel(t.Context())
defer cancel()
tx, err := testDB.Begin(ctxTx)
if err != nil {
t.Fatalf("Error starting transaction: %v", err)
}
defer func() {
if err := tx.Rollback(ctxTx); err != nil {
t.Fatalf("Error rolling back transaction: %v", err)
}
}()
query, args, err := bob.Build(ctxTx, psql.Select(UserByUsername(random_string(nil))))
if err != nil {
t.Fatal(err)
}
rows, err := tx.QueryContext(ctxTx, query, args...)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
t.Fatal(err)
}
if len(columns) != 12 {
t.Fatalf("expected %d columns, got %d", 12, len(columns))
}
if columns[0] != "id" {
t.Fatalf("expected column %d to be %s, got %s", 0, "id", columns[0])
}
if columns[1] != "arcgis_access_token" {
t.Fatalf("expected column %d to be %s, got %s", 1, "arcgis_access_token", columns[1])
}
if columns[2] != "arcgis_license" {
t.Fatalf("expected column %d to be %s, got %s", 2, "arcgis_license", columns[2])
}
if columns[3] != "arcgis_refresh_token" {
t.Fatalf("expected column %d to be %s, got %s", 3, "arcgis_refresh_token", columns[3])
}
if columns[4] != "arcgis_refresh_token_expires" {
t.Fatalf("expected column %d to be %s, got %s", 4, "arcgis_refresh_token_expires", columns[4])
}
if columns[5] != "arcgis_role" {
t.Fatalf("expected column %d to be %s, got %s", 5, "arcgis_role", columns[5])
}
if columns[6] != "display_name" {
t.Fatalf("expected column %d to be %s, got %s", 6, "display_name", columns[6])
}
if columns[7] != "email" {
t.Fatalf("expected column %d to be %s, got %s", 7, "email", columns[7])
}
if columns[8] != "organization_id" {
t.Fatalf("expected column %d to be %s, got %s", 8, "organization_id", columns[8])
}
if columns[9] != "username" {
t.Fatalf("expected column %d to be %s, got %s", 9, "username", columns[9])
}
if columns[10] != "password_hash_type" {
t.Fatalf("expected column %d to be %s, got %s", 10, "password_hash_type", columns[10])
}
if columns[11] != "password_hash" {
t.Fatalf("expected column %d to be %s, got %s", 11, "password_hash", columns[11])
}
})
}

4
sql/user_by_username.sql Normal file
View file

@ -0,0 +1,4 @@
-- UserByUsername
SELECT * FROM user_ WHERE
username = $1 AND
password_hash_type = 'bcrypt-14';

9
templates/dashboard.html Normal file
View file

@ -0,0 +1,9 @@
{{template "base.html" .}}
{{define "title"}}Dash{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
<h1>Hey {{ .Username }}</h1>
<p>At this point, pretend I'm showing you the result of some ArcGIS data.</p>
{{end}}

View file

@ -33,22 +33,23 @@
<p class="text-muted">Please enter your credentials</p>
</div>
<form>
<form method="POST" action="/signin">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" class="form-control" id="username" required>
<input type="text" class="form-control" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" class="form-control" id="password" required>
<input type="password" class="form-control" name="password" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember">
<label class="form-check-label" for="remember">Remember me</label>
{{ if .InvalidCredentials }}
<div class="alert alert-danger" role="alert">
The credentials you provided weren't recognized.
</div>
{{ end }}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Login</button>
</div>