diff --git a/.air.toml b/.air.toml new file mode 100644 index 00000000..d5bf4207 --- /dev/null +++ b/.air.toml @@ -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 diff --git a/auth.go b/auth.go new file mode 100644 index 00000000..222fc877 --- /dev/null +++ b/auth.go @@ -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.") + + } +} diff --git a/database.go b/database.go index 17fd4b65..e707f4cc 100644 --- a/database.go +++ b/database.go @@ -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 { diff --git a/dberrors/sessions.bob.go b/dberrors/sessions.bob.go new file mode 100644 index 00000000..99196ded --- /dev/null +++ b/dberrors/sessions.bob.go @@ -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 +} diff --git a/dbinfo/sessions.bob.go b/dbinfo/sessions.bob.go new file mode 100644 index 00000000..848632ef --- /dev/null +++ b/dbinfo/sessions.bob.go @@ -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{} +} diff --git a/endpoint.go b/endpoint.go index 52e55291..390b79fc 100644 --- a/endpoint.go +++ b/endpoint.go @@ -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) } diff --git a/factory/bobfactory_context.bob.go b/factory/bobfactory_context.bob.go index 631bc77b..352032a8 100644 --- a/factory/bobfactory_context.bob.go +++ b/factory/bobfactory_context.bob.go @@ -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") diff --git a/factory/bobfactory_main.bob.go b/factory/bobfactory_main.bob.go index b71b0ba7..8170dcb3 100644 --- a/factory/bobfactory_main.bob.go +++ b/factory/bobfactory_main.bob.go @@ -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 } diff --git a/factory/bobfactory_main.bob_test.go b/factory/bobfactory_main.bob_test.go index 51a5a23e..c3db8f15 100644 --- a/factory/bobfactory_main.bob_test.go +++ b/factory/bobfactory_main.bob_test.go @@ -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") diff --git a/factory/bobfactory_random.bob.go b/factory/bobfactory_random.bob.go index 7d409353..2a79a9b6 100644 --- a/factory/bobfactory_random.bob.go +++ b/factory/bobfactory_random.bob.go @@ -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 diff --git a/factory/bobfactory_random.bob_test.go b/factory/bobfactory_random.bob_test.go index 112fcf04..adc34e89 100644 --- a/factory/bobfactory_random.bob_test.go +++ b/factory/bobfactory_random.bob_test.go @@ -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() diff --git a/factory/sessions.bob.go b/factory/sessions.bob.go new file mode 100644 index 00000000..ee9fa0fa --- /dev/null +++ b/factory/sessions.bob.go @@ -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) + }) +} diff --git a/flake.nix b/flake.nix index 1846a9a8..49c27016 100644 --- a/flake.nix +++ b/flake.nix @@ -19,6 +19,7 @@ # Development shell configuration devShells.default = pkgs.mkShell { buildInputs = [ + pkgs.air pkgs.go pkgs.goose pkgs.gotools diff --git a/go.mod b/go.mod index 8f648c83..8ea38edf 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index bd7e148a..6dae47a3 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/html.go b/html.go index 4ea3c8d0..604228fe 100644 --- a/html.go +++ b/html.go @@ -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 } diff --git a/main.go b/main.go index 6d9ac58d..c69a9a1e 100644 --- a/main.go +++ b/main.go @@ -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) diff --git a/migrations/00003_add_session.sql b/migrations/00003_add_session.sql new file mode 100644 index 00000000..cc773f74 --- /dev/null +++ b/migrations/00003_add_session.sql @@ -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; diff --git a/models/bob_types.bob_test.go b/models/bob_types.bob_test.go index 80312858..f4846f58 100644 --- a/models/bob_types.bob_test.go +++ b/models/bob_types.bob_test.go @@ -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{} diff --git a/models/bob_where.bob.go b/models/bob_where.bob.go index 13f1536c..88e47b87 100644 --- a/models/bob_where.bob.go +++ b/models/bob_where.bob.go @@ -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), } } diff --git a/models/sessions.bob.go b/models/sessions.bob.go new file mode 100644 index 00000000..2a97b14f --- /dev/null +++ b/models/sessions.bob.go @@ -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), + } +} diff --git a/sql/test_utils.bob_test.go b/sql/test_utils.bob_test.go new file mode 100644 index 00000000..866c64d1 --- /dev/null +++ b/sql/test_utils.bob_test.go @@ -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) +} diff --git a/sql/user_by_username.bob.go b/sql/user_by_username.bob.go new file mode 100644 index 00000000..25e33ff5 --- /dev/null +++ b/sql/user_by_username.bob.go @@ -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) +} diff --git a/sql/user_by_username.bob.sql b/sql/user_by_username.bob.sql new file mode 100644 index 00000000..9a41ecb8 --- /dev/null +++ b/sql/user_by_username.bob.sql @@ -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'; diff --git a/sql/user_by_username.bob_test.go b/sql/user_by_username.bob_test.go new file mode 100644 index 00000000..ba9ca8ac --- /dev/null +++ b/sql/user_by_username.bob_test.go @@ -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]) + } + }) +} diff --git a/sql/user_by_username.sql b/sql/user_by_username.sql new file mode 100644 index 00000000..64d96249 --- /dev/null +++ b/sql/user_by_username.sql @@ -0,0 +1,4 @@ +-- UserByUsername +SELECT * FROM user_ WHERE + username = $1 AND + password_hash_type = 'bcrypt-14'; diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 00000000..ca9a845c --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,9 @@ +{{template "base.html" .}} + +{{define "title"}}Dash{{end}} +{{define "style"}} +{{end}} +{{define "content"}} +
At this point, pretend I'm showing you the result of some ArcGIS data.
+{{end}} diff --git a/templates/root.html b/templates/signin.html similarity index 85% rename from templates/root.html rename to templates/signin.html index 45099022..3d85ef5a 100644 --- a/templates/root.html +++ b/templates/signin.html @@ -33,22 +33,23 @@Please enter your credentials
-