diff --git a/arcgis.go b/arcgis.go new file mode 100644 index 00000000..c6ff141f --- /dev/null +++ b/arcgis.go @@ -0,0 +1,122 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/aarondl/opt/omit" + "github.com/Gleipnir-Technology/nidus-sync/models" +) + +var CodeVerifier string = "random_secure_string_min_43_chars_long_should_be_stored_in_session" + +type OAuthTokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Username string `json:"username"` +} + +// Build the ArcGIS authorization URL with PKCE +func buildArcGISAuthURL(clientID string, redirectURI string, expiration int) string { + baseURL := "https://www.arcgis.com/sharing/rest/oauth2/authorize/" + + params := url.Values{} + params.Add("client_id", clientID) + params.Add("redirect_uri", redirectURI) + params.Add("response_type", "code") + //params.Add("code_challenge", generateCodeChallenge(codeVerifier)) + //params.Add("code_challenge_method", "S256") + params.Add("expiration", strconv.Itoa(expiration)) + + return baseURL + "?" + params.Encode() +} + +func futureUTCTimestamp(secondsFromNow int) time.Time { + return time.Now().UTC().Add(time.Duration(secondsFromNow) * time.Second) +} + +// Helper function to generate code challenge from code verifier +func generateCodeChallenge(codeVerifier string) string { + hash := sha256.Sum256([]byte(codeVerifier)) + return base64.RawURLEncoding.EncodeToString(hash[:]) +} + +// Generate a random code verifier for PKCE +func generateCodeVerifier() string { + bytes := make([]byte, 64) // 64 bytes = 512 bits + rand.Read(bytes) + return base64.RawURLEncoding.EncodeToString(bytes) +} + +func handleOauthAccessCode(ctx context.Context, user *models.User, code string) error { + baseURL := "https://www.arcgis.com/sharing/rest/oauth2/token/" + + //params.Add("code_verifier", "S256") + + form := url.Values{ + "grant_type": []string{"authorization_code"}, + "code": []string{code}, + "client_id": []string{ClientID}, + "redirect_uri": []string{redirectURL()}, + } + + req, err := http.NewRequest("POST", baseURL, strings.NewReader(form.Encode())) + if err != nil { + return fmt.Errorf("Failed to create request: %v", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + client := http.Client{} + log.Printf("POST %s", baseURL) + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Failed to do request: %v", err) + } + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + log.Printf("Response %d", resp.StatusCode) + if resp.StatusCode >= http.StatusBadRequest { + if err != nil { + return fmt.Errorf("Got status code %d and failed to read response body: %v", resp.StatusCode, err) + } + bodyString := string(bodyBytes) + var errorResp map[string]interface{} + if err := json.Unmarshal(bodyBytes, &errorResp); err == nil { + return fmt.Errorf("API response JSON error: %d: %v", resp.StatusCode, errorResp) + } + return fmt.Errorf("API returned error status %d: %s", resp.StatusCode, bodyString) + } + var tokenResponse OAuthTokenResponse + err = json.Unmarshal(bodyBytes, &tokenResponse) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON: %v", err) + } + log.Printf("Refresh token '%s'", tokenResponse.RefreshToken) + + setter := models.OauthTokenSetter{ + AccessToken: omit.From(tokenResponse.AccessToken), + Expires: omit.From(futureUTCTimestamp(tokenResponse.ExpiresIn)), + RefreshToken: omit.From(tokenResponse.RefreshToken), + Username: omit.From(tokenResponse.Username), + } + err = user.InsertUserOauthTokens(ctx, PGInstance.BobDB, &setter) + if err != nil { + return fmt.Errorf("Failed to save token to database: %v", err) + } + return nil +} + +func redirectURL() string { + return BaseURL + "/arcgis/oauth/callback" +} diff --git a/dberrors/oauth_token.bob.go b/dberrors/oauth_token.bob.go new file mode 100644 index 00000000..2517c6e5 --- /dev/null +++ b/dberrors/oauth_token.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 OauthTokenErrors = &oauthTokenErrors{ + ErrUniqueOauthTokenPkey: &UniqueConstraintError{ + schema: "", + table: "oauth_token", + columns: []string{"id"}, + s: "oauth_token_pkey", + }, +} + +type oauthTokenErrors struct { + ErrUniqueOauthTokenPkey *UniqueConstraintError +} diff --git a/dbinfo/oauth_token.bob.go b/dbinfo/oauth_token.bob.go new file mode 100644 index 00000000..f25b58f1 --- /dev/null +++ b/dbinfo/oauth_token.bob.go @@ -0,0 +1,157 @@ +// 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 OauthTokens = Table[ + oauthTokenColumns, + oauthTokenIndexes, + oauthTokenForeignKeys, + oauthTokenUniques, + oauthTokenChecks, +]{ + Schema: "", + Name: "oauth_token", + Columns: oauthTokenColumns{ + ID: column{ + Name: "id", + DBType: "integer", + Default: "nextval('oauth_token_id_seq'::regclass)", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + AccessToken: column{ + Name: "access_token", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Expires: column{ + Name: "expires", + DBType: "timestamp without time zone", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + RefreshToken: column{ + Name: "refresh_token", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Username: column{ + Name: "username", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + UserID: column{ + Name: "user_id", + DBType: "integer", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + }, + Indexes: oauthTokenIndexes{ + OauthTokenPkey: index{ + Type: "btree", + Name: "oauth_token_pkey", + Columns: []indexColumn{ + { + Name: "id", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, + }, + PrimaryKey: &constraint{ + Name: "oauth_token_pkey", + Columns: []string{"id"}, + Comment: "", + }, + ForeignKeys: oauthTokenForeignKeys{ + OauthTokenOauthTokenUserIDFkey: foreignKey{ + constraint: constraint{ + Name: "oauth_token.oauth_token_user_id_fkey", + Columns: []string{"user_id"}, + Comment: "", + }, + ForeignTable: "user_", + ForeignColumns: []string{"id"}, + }, + }, + + Comment: "", +} + +type oauthTokenColumns struct { + ID column + AccessToken column + Expires column + RefreshToken column + Username column + UserID column +} + +func (c oauthTokenColumns) AsSlice() []column { + return []column{ + c.ID, c.AccessToken, c.Expires, c.RefreshToken, c.Username, c.UserID, + } +} + +type oauthTokenIndexes struct { + OauthTokenPkey index +} + +func (i oauthTokenIndexes) AsSlice() []index { + return []index{ + i.OauthTokenPkey, + } +} + +type oauthTokenForeignKeys struct { + OauthTokenOauthTokenUserIDFkey foreignKey +} + +func (f oauthTokenForeignKeys) AsSlice() []foreignKey { + return []foreignKey{ + f.OauthTokenOauthTokenUserIDFkey, + } +} + +type oauthTokenUniques struct{} + +func (u oauthTokenUniques) AsSlice() []constraint { + return []constraint{} +} + +type oauthTokenChecks struct{} + +func (c oauthTokenChecks) AsSlice() []check { + return []check{} +} diff --git a/endpoint.go b/endpoint.go index 48e7604a..89d00975 100644 --- a/endpoint.go +++ b/endpoint.go @@ -10,6 +10,32 @@ import ( "github.com/go-chi/chi/v5" "github.com/skip2/go-qrcode" ) + +func getArcgisOauthBegin(w http.ResponseWriter, r *http.Request) { + expiration := 60 + authURL := buildArcGISAuthURL(ClientID, redirectURL(), expiration) + http.Redirect(w, r, authURL, http.StatusFound) +} + +func getArcgisOauthCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + slog.Info("Handling oauth callback", slog.String("code", code)) + if code == "" { + respondError(w, "Access code is empty", nil, http.StatusBadRequest) + return + } + user, err := getAuthenticatedUser(r) + if err != nil { + respondError(w, "You're not currently authenticated, which really shouldn't happen.", err, http.StatusUnauthorized) + return + } + err = handleOauthAccessCode(r.Context(), user, code) + if err != nil { + respondError(w, "Failed to handle access code", err, http.StatusInternalServerError) + return + } + http.Redirect(w, r, BaseURL+"/", http.StatusFound) +} func getFavicon(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "image/x-icon") diff --git a/factory/bobfactory_context.bob.go b/factory/bobfactory_context.bob.go index 352032a8..a9176222 100644 --- a/factory/bobfactory_context.bob.go +++ b/factory/bobfactory_context.bob.go @@ -11,6 +11,10 @@ var ( // Relationship Contexts for goose_db_version gooseDBVersionWithParentsCascadingCtx = newContextual[bool]("gooseDBVersionWithParentsCascading") + // Relationship Contexts for oauth_token + oauthTokenWithParentsCascadingCtx = newContextual[bool]("oauthTokenWithParentsCascading") + oauthTokenRelUserUserCtx = newContextual[bool]("oauth_token.user_.oauth_token.oauth_token_user_id_fkey") + // Relationship Contexts for organization organizationWithParentsCascadingCtx = newContextual[bool]("organizationWithParentsCascading") organizationRelUserCtx = newContextual[bool]("organization.user_.user_.user__organization_id_fkey") @@ -20,6 +24,7 @@ var ( // Relationship Contexts for user_ userWithParentsCascadingCtx = newContextual[bool]("userWithParentsCascading") + userRelUserOauthTokensCtx = newContextual[bool]("oauth_token.user_.oauth_token.oauth_token_user_id_fkey") 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 359cc986..6340638f 100644 --- a/factory/bobfactory_main.bob.go +++ b/factory/bobfactory_main.bob.go @@ -14,6 +14,7 @@ import ( type Factory struct { baseGooseDBVersionMods GooseDBVersionModSlice + baseOauthTokenMods OauthTokenModSlice baseOrganizationMods OrganizationModSlice baseSessionMods SessionModSlice baseUserMods UserModSlice @@ -50,6 +51,40 @@ func (f *Factory) FromExistingGooseDBVersion(m *models.GooseDBVersion) *GooseDBV return o } +func (f *Factory) NewOauthToken(mods ...OauthTokenMod) *OauthTokenTemplate { + return f.NewOauthTokenWithContext(context.Background(), mods...) +} + +func (f *Factory) NewOauthTokenWithContext(ctx context.Context, mods ...OauthTokenMod) *OauthTokenTemplate { + o := &OauthTokenTemplate{f: f} + + if f != nil { + f.baseOauthTokenMods.Apply(ctx, o) + } + + OauthTokenModSlice(mods).Apply(ctx, o) + + return o +} + +func (f *Factory) FromExistingOauthToken(m *models.OauthToken) *OauthTokenTemplate { + o := &OauthTokenTemplate{f: f, alreadyPersisted: true} + + o.ID = func() int32 { return m.ID } + o.AccessToken = func() string { return m.AccessToken } + o.Expires = func() time.Time { return m.Expires } + o.RefreshToken = func() string { return m.RefreshToken } + o.Username = func() string { return m.Username } + o.UserID = func() int32 { return m.UserID } + + ctx := context.Background() + if m.R.UserUser != nil { + OauthTokenMods.WithExistingUserUser(m.R.UserUser).Apply(ctx, o) + } + + return o +} + func (f *Factory) NewOrganization(mods ...OrganizationMod) *OrganizationTemplate { return f.NewOrganizationWithContext(context.Background(), mods...) } @@ -139,6 +174,9 @@ func (f *Factory) FromExistingUser(m *models.User) *UserTemplate { o.PasswordHash = func() string { return m.PasswordHash } ctx := context.Background() + if len(m.R.UserOauthTokens) > 0 { + UserMods.AddExistingUserOauthTokens(m.R.UserOauthTokens...).Apply(ctx, o) + } if m.R.Organization != nil { UserMods.WithExistingOrganization(m.R.Organization).Apply(ctx, o) } @@ -154,6 +192,14 @@ func (f *Factory) AddBaseGooseDBVersionMod(mods ...GooseDBVersionMod) { f.baseGooseDBVersionMods = append(f.baseGooseDBVersionMods, mods...) } +func (f *Factory) ClearBaseOauthTokenMods() { + f.baseOauthTokenMods = nil +} + +func (f *Factory) AddBaseOauthTokenMod(mods ...OauthTokenMod) { + f.baseOauthTokenMods = append(f.baseOauthTokenMods, mods...) +} + func (f *Factory) ClearBaseOrganizationMods() { f.baseOrganizationMods = nil } diff --git a/factory/bobfactory_main.bob_test.go b/factory/bobfactory_main.bob_test.go index c3db8f15..0001ac1c 100644 --- a/factory/bobfactory_main.bob_test.go +++ b/factory/bobfactory_main.bob_test.go @@ -32,6 +32,30 @@ func TestCreateGooseDBVersion(t *testing.T) { } } +func TestCreateOauthToken(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().NewOauthTokenWithContext(ctx).Create(ctx, tx); err != nil { + t.Fatalf("Error creating OauthToken: %v", err) + } +} + func TestCreateOrganization(t *testing.T) { if testDB == nil { t.Skip("skipping test, no DSN provided") diff --git a/factory/oauth_token.bob.go b/factory/oauth_token.bob.go new file mode 100644 index 00000000..18d06698 --- /dev/null +++ b/factory/oauth_token.bob.go @@ -0,0 +1,542 @@ +// 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 OauthTokenMod interface { + Apply(context.Context, *OauthTokenTemplate) +} + +type OauthTokenModFunc func(context.Context, *OauthTokenTemplate) + +func (f OauthTokenModFunc) Apply(ctx context.Context, n *OauthTokenTemplate) { + f(ctx, n) +} + +type OauthTokenModSlice []OauthTokenMod + +func (mods OauthTokenModSlice) Apply(ctx context.Context, n *OauthTokenTemplate) { + for _, f := range mods { + f.Apply(ctx, n) + } +} + +// OauthTokenTemplate is an object representing the database table. +// all columns are optional and should be set by mods +type OauthTokenTemplate struct { + ID func() int32 + AccessToken func() string + Expires func() time.Time + RefreshToken func() string + Username func() string + UserID func() int32 + + r oauthTokenR + f *Factory + + alreadyPersisted bool +} + +type oauthTokenR struct { + UserUser *oauthTokenRUserUserR +} + +type oauthTokenRUserUserR struct { + o *UserTemplate +} + +// Apply mods to the OauthTokenTemplate +func (o *OauthTokenTemplate) Apply(ctx context.Context, mods ...OauthTokenMod) { + for _, mod := range mods { + mod.Apply(ctx, o) + } +} + +// setModelRels creates and sets the relationships on *models.OauthToken +// according to the relationships in the template. Nothing is inserted into the db +func (t OauthTokenTemplate) setModelRels(o *models.OauthToken) { + if t.r.UserUser != nil { + rel := t.r.UserUser.o.Build() + rel.R.UserOauthTokens = append(rel.R.UserOauthTokens, o) + o.UserID = rel.ID // h2 + o.R.UserUser = rel + } +} + +// BuildSetter returns an *models.OauthTokenSetter +// this does nothing with the relationship templates +func (o OauthTokenTemplate) BuildSetter() *models.OauthTokenSetter { + m := &models.OauthTokenSetter{} + + if o.ID != nil { + val := o.ID() + m.ID = omit.From(val) + } + if o.AccessToken != nil { + val := o.AccessToken() + m.AccessToken = omit.From(val) + } + if o.Expires != nil { + val := o.Expires() + m.Expires = omit.From(val) + } + if o.RefreshToken != nil { + val := o.RefreshToken() + m.RefreshToken = omit.From(val) + } + if o.Username != nil { + val := o.Username() + m.Username = omit.From(val) + } + if o.UserID != nil { + val := o.UserID() + m.UserID = omit.From(val) + } + + return m +} + +// BuildManySetter returns an []*models.OauthTokenSetter +// this does nothing with the relationship templates +func (o OauthTokenTemplate) BuildManySetter(number int) []*models.OauthTokenSetter { + m := make([]*models.OauthTokenSetter, number) + + for i := range m { + m[i] = o.BuildSetter() + } + + return m +} + +// Build returns an *models.OauthToken +// Related objects are also created and placed in the .R field +// NOTE: Objects are not inserted into the database. Use OauthTokenTemplate.Create +func (o OauthTokenTemplate) Build() *models.OauthToken { + m := &models.OauthToken{} + + if o.ID != nil { + m.ID = o.ID() + } + if o.AccessToken != nil { + m.AccessToken = o.AccessToken() + } + if o.Expires != nil { + m.Expires = o.Expires() + } + if o.RefreshToken != nil { + m.RefreshToken = o.RefreshToken() + } + if o.Username != nil { + m.Username = o.Username() + } + if o.UserID != nil { + m.UserID = o.UserID() + } + + o.setModelRels(m) + + return m +} + +// BuildMany returns an models.OauthTokenSlice +// Related objects are also created and placed in the .R field +// NOTE: Objects are not inserted into the database. Use OauthTokenTemplate.CreateMany +func (o OauthTokenTemplate) BuildMany(number int) models.OauthTokenSlice { + m := make(models.OauthTokenSlice, number) + + for i := range m { + m[i] = o.Build() + } + + return m +} + +func ensureCreatableOauthToken(m *models.OauthTokenSetter) { + if !(m.AccessToken.IsValue()) { + val := random_string(nil) + m.AccessToken = omit.From(val) + } + if !(m.Expires.IsValue()) { + val := random_time_Time(nil) + m.Expires = omit.From(val) + } + if !(m.RefreshToken.IsValue()) { + val := random_string(nil) + m.RefreshToken = omit.From(val) + } + if !(m.Username.IsValue()) { + val := random_string(nil) + m.Username = omit.From(val) + } + if !(m.UserID.IsValue()) { + val := random_int32(nil) + m.UserID = omit.From(val) + } +} + +// insertOptRels creates and inserts any optional the relationships on *models.OauthToken +// according to the relationships in the template. +// any required relationship should have already exist on the model +func (o *OauthTokenTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.OauthToken) error { + var err error + + return err +} + +// Create builds a oauthToken and inserts it into the database +// Relations objects are also inserted and placed in the .R field +func (o *OauthTokenTemplate) Create(ctx context.Context, exec bob.Executor) (*models.OauthToken, error) { + var err error + opt := o.BuildSetter() + ensureCreatableOauthToken(opt) + + if o.r.UserUser == nil { + OauthTokenMods.WithNewUserUser().Apply(ctx, o) + } + + var rel0 *models.User + + if o.r.UserUser.o.alreadyPersisted { + rel0 = o.r.UserUser.o.Build() + } else { + rel0, err = o.r.UserUser.o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + opt.UserID = omit.From(rel0.ID) + + m, err := models.OauthTokens.Insert(opt).One(ctx, exec) + if err != nil { + return nil, err + } + + m.R.UserUser = rel0 + + if err := o.insertOptRels(ctx, exec, m); err != nil { + return nil, err + } + return m, err +} + +// MustCreate builds a oauthToken and inserts it into the database +// Relations objects are also inserted and placed in the .R field +// panics if an error occurs +func (o *OauthTokenTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.OauthToken { + m, err := o.Create(ctx, exec) + if err != nil { + panic(err) + } + return m +} + +// CreateOrFail builds a oauthToken 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 *OauthTokenTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.OauthToken { + tb.Helper() + m, err := o.Create(ctx, exec) + if err != nil { + tb.Fatal(err) + return nil + } + return m +} + +// CreateMany builds multiple oauthTokens and inserts them into the database +// Relations objects are also inserted and placed in the .R field +func (o OauthTokenTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.OauthTokenSlice, error) { + var err error + m := make(models.OauthTokenSlice, number) + + for i := range m { + m[i], err = o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + return m, nil +} + +// MustCreateMany builds multiple oauthTokens and inserts them into the database +// Relations objects are also inserted and placed in the .R field +// panics if an error occurs +func (o OauthTokenTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.OauthTokenSlice { + m, err := o.CreateMany(ctx, exec, number) + if err != nil { + panic(err) + } + return m +} + +// CreateManyOrFail builds multiple oauthTokens 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 OauthTokenTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.OauthTokenSlice { + tb.Helper() + m, err := o.CreateMany(ctx, exec, number) + if err != nil { + tb.Fatal(err) + return nil + } + return m +} + +// OauthToken has methods that act as mods for the OauthTokenTemplate +var OauthTokenMods oauthTokenMods + +type oauthTokenMods struct{} + +func (m oauthTokenMods) RandomizeAllColumns(f *faker.Faker) OauthTokenMod { + return OauthTokenModSlice{ + OauthTokenMods.RandomID(f), + OauthTokenMods.RandomAccessToken(f), + OauthTokenMods.RandomExpires(f), + OauthTokenMods.RandomRefreshToken(f), + OauthTokenMods.RandomUsername(f), + OauthTokenMods.RandomUserID(f), + } +} + +// Set the model columns to this value +func (m oauthTokenMods) ID(val int32) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.ID = func() int32 { return val } + }) +} + +// Set the Column from the function +func (m oauthTokenMods) IDFunc(f func() int32) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.ID = f + }) +} + +// Clear any values for the column +func (m oauthTokenMods) UnsetID() OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.ID = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m oauthTokenMods) RandomID(f *faker.Faker) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.ID = func() int32 { + return random_int32(f) + } + }) +} + +// Set the model columns to this value +func (m oauthTokenMods) AccessToken(val string) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.AccessToken = func() string { return val } + }) +} + +// Set the Column from the function +func (m oauthTokenMods) AccessTokenFunc(f func() string) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.AccessToken = f + }) +} + +// Clear any values for the column +func (m oauthTokenMods) UnsetAccessToken() OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.AccessToken = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m oauthTokenMods) RandomAccessToken(f *faker.Faker) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.AccessToken = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m oauthTokenMods) Expires(val time.Time) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Expires = func() time.Time { return val } + }) +} + +// Set the Column from the function +func (m oauthTokenMods) ExpiresFunc(f func() time.Time) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Expires = f + }) +} + +// Clear any values for the column +func (m oauthTokenMods) UnsetExpires() OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Expires = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m oauthTokenMods) RandomExpires(f *faker.Faker) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Expires = func() time.Time { + return random_time_Time(f) + } + }) +} + +// Set the model columns to this value +func (m oauthTokenMods) RefreshToken(val string) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.RefreshToken = func() string { return val } + }) +} + +// Set the Column from the function +func (m oauthTokenMods) RefreshTokenFunc(f func() string) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.RefreshToken = f + }) +} + +// Clear any values for the column +func (m oauthTokenMods) UnsetRefreshToken() OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.RefreshToken = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m oauthTokenMods) RandomRefreshToken(f *faker.Faker) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.RefreshToken = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m oauthTokenMods) Username(val string) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Username = func() string { return val } + }) +} + +// Set the Column from the function +func (m oauthTokenMods) UsernameFunc(f func() string) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Username = f + }) +} + +// Clear any values for the column +func (m oauthTokenMods) UnsetUsername() OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Username = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m oauthTokenMods) RandomUsername(f *faker.Faker) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.Username = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m oauthTokenMods) UserID(val int32) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.UserID = func() int32 { return val } + }) +} + +// Set the Column from the function +func (m oauthTokenMods) UserIDFunc(f func() int32) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.UserID = f + }) +} + +// Clear any values for the column +func (m oauthTokenMods) UnsetUserID() OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.UserID = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m oauthTokenMods) RandomUserID(f *faker.Faker) OauthTokenMod { + return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { + o.UserID = func() int32 { + return random_int32(f) + } + }) +} + +func (m oauthTokenMods) WithParentsCascading() OauthTokenMod { + return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) { + if isDone, _ := oauthTokenWithParentsCascadingCtx.Value(ctx); isDone { + return + } + ctx = oauthTokenWithParentsCascadingCtx.WithValue(ctx, true) + { + + related := o.f.NewUserWithContext(ctx, UserMods.WithParentsCascading()) + m.WithUserUser(related).Apply(ctx, o) + } + }) +} + +func (m oauthTokenMods) WithUserUser(rel *UserTemplate) OauthTokenMod { + return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) { + o.r.UserUser = &oauthTokenRUserUserR{ + o: rel, + } + }) +} + +func (m oauthTokenMods) WithNewUserUser(mods ...UserMod) OauthTokenMod { + return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) { + related := o.f.NewUserWithContext(ctx, mods...) + + m.WithUserUser(related).Apply(ctx, o) + }) +} + +func (m oauthTokenMods) WithExistingUserUser(em *models.User) OauthTokenMod { + return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) { + o.r.UserUser = &oauthTokenRUserUserR{ + o: o.f.FromExistingUser(em), + } + }) +} + +func (m oauthTokenMods) WithoutUserUser() OauthTokenMod { + return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) { + o.r.UserUser = nil + }) +} diff --git a/factory/user_.bob.go b/factory/user_.bob.go index 2d0b830d..2ec98b0d 100644 --- a/factory/user_.bob.go +++ b/factory/user_.bob.go @@ -58,9 +58,14 @@ type UserTemplate struct { } type userR struct { - Organization *userROrganizationR + UserOauthTokens []*userRUserOauthTokensR + Organization *userROrganizationR } +type userRUserOauthTokensR struct { + number int + o *OauthTokenTemplate +} type userROrganizationR struct { o *OrganizationTemplate } @@ -75,6 +80,19 @@ func (o *UserTemplate) Apply(ctx context.Context, mods ...UserMod) { // setModelRels creates and sets the relationships on *models.User // according to the relationships in the template. Nothing is inserted into the db func (t UserTemplate) setModelRels(o *models.User) { + if t.r.UserOauthTokens != nil { + rel := models.OauthTokenSlice{} + for _, r := range t.r.UserOauthTokens { + related := r.o.BuildMany(r.number) + for _, rel := range related { + rel.UserID = o.ID // h2 + rel.R.UserUser = o + } + rel = append(rel, related...) + } + o.R.UserOauthTokens = rel + } + if t.r.Organization != nil { rel := t.r.Organization.o.Build() rel.R.User = append(rel.R.User, o) @@ -238,18 +256,38 @@ func ensureCreatableUser(m *models.UserSetter) { func (o *UserTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.User) error { var err error + isUserOauthTokensDone, _ := userRelUserOauthTokensCtx.Value(ctx) + if !isUserOauthTokensDone && o.r.UserOauthTokens != nil { + ctx = userRelUserOauthTokensCtx.WithValue(ctx, true) + for _, r := range o.r.UserOauthTokens { + if r.o.alreadyPersisted { + m.R.UserOauthTokens = append(m.R.UserOauthTokens, r.o.Build()) + } else { + rel0, err := r.o.CreateMany(ctx, exec, r.number) + if err != nil { + return err + } + + err = m.AttachUserOauthTokens(ctx, exec, rel0...) + if err != nil { + return err + } + } + } + } + isOrganizationDone, _ := userRelOrganizationCtx.Value(ctx) if !isOrganizationDone && o.r.Organization != nil { ctx = userRelOrganizationCtx.WithValue(ctx, true) if o.r.Organization.o.alreadyPersisted { m.R.Organization = o.r.Organization.o.Build() } else { - var rel0 *models.Organization - rel0, err = o.r.Organization.o.Create(ctx, exec) + var rel1 *models.Organization + rel1, err = o.r.Organization.o.Create(ctx, exec) if err != nil { return err } - err = m.AttachOrganization(ctx, exec, rel0) + err = m.AttachOrganization(ctx, exec, rel1) if err != nil { return err } @@ -933,3 +971,51 @@ func (m userMods) WithoutOrganization() UserMod { o.r.Organization = nil }) } + +func (m userMods) WithUserOauthTokens(number int, related *OauthTokenTemplate) UserMod { + return UserModFunc(func(ctx context.Context, o *UserTemplate) { + o.r.UserOauthTokens = []*userRUserOauthTokensR{{ + number: number, + o: related, + }} + }) +} + +func (m userMods) WithNewUserOauthTokens(number int, mods ...OauthTokenMod) UserMod { + return UserModFunc(func(ctx context.Context, o *UserTemplate) { + related := o.f.NewOauthTokenWithContext(ctx, mods...) + m.WithUserOauthTokens(number, related).Apply(ctx, o) + }) +} + +func (m userMods) AddUserOauthTokens(number int, related *OauthTokenTemplate) UserMod { + return UserModFunc(func(ctx context.Context, o *UserTemplate) { + o.r.UserOauthTokens = append(o.r.UserOauthTokens, &userRUserOauthTokensR{ + number: number, + o: related, + }) + }) +} + +func (m userMods) AddNewUserOauthTokens(number int, mods ...OauthTokenMod) UserMod { + return UserModFunc(func(ctx context.Context, o *UserTemplate) { + related := o.f.NewOauthTokenWithContext(ctx, mods...) + m.AddUserOauthTokens(number, related).Apply(ctx, o) + }) +} + +func (m userMods) AddExistingUserOauthTokens(existingModels ...*models.OauthToken) UserMod { + return UserModFunc(func(ctx context.Context, o *UserTemplate) { + for _, em := range existingModels { + o.r.UserOauthTokens = append(o.r.UserOauthTokens, &userRUserOauthTokensR{ + o: o.f.FromExistingOauthToken(em), + }) + } + }) +} + +func (m userMods) WithoutUserOauthTokens() UserMod { + return UserModFunc(func(ctx context.Context, o *UserTemplate) { + o.r.UserOauthTokens = nil + }) +} diff --git a/main.go b/main.go index e90f7173..0be40640 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,8 @@ func main() { r.Use(sessionManager.LoadAndSave) r.Get("/", getRoot) + r.Get("/arcgis/oauth/begin", getArcgisOauthBegin) + r.Get("/arcgis/oauth/callback", getArcgisOauthCallback) r.Get("/qr-code/report/{code}", getQRCodeReport) r.Get("/report", getReport) r.Get("/report/{code}", getReportDetail) diff --git a/migrations/00004_add_oauth_token.sql b/migrations/00004_add_oauth_token.sql new file mode 100644 index 00000000..c2ac0841 --- /dev/null +++ b/migrations/00004_add_oauth_token.sql @@ -0,0 +1,12 @@ +-- +goose Up +CREATE TABLE oauth_token ( + id SERIAL PRIMARY KEY, + access_token TEXT NOT NULL, + expires TIMESTAMP NOT NULL, + refresh_token TEXT NOT NULL, + username TEXT NOT NULL, + user_id INTEGER REFERENCES user_ (id) NOT NULL +); + +-- +goose Down +DROP TABLE oauth_token; diff --git a/models/bob_joins.bob.go b/models/bob_joins.bob.go index 7058198a..f947bebe 100644 --- a/models/bob_joins.bob.go +++ b/models/bob_joins.bob.go @@ -32,6 +32,7 @@ func (j joinSet[Q]) AliasedAs(alias string) joinSet[Q] { } type joins[Q dialect.Joinable] struct { + OauthTokens joinSet[oauthTokenJoins[Q]] Organizations joinSet[organizationJoins[Q]] Users joinSet[userJoins[Q]] } @@ -46,6 +47,7 @@ func buildJoinSet[Q interface{ aliasedAs(string) Q }, C any, F func(C, string) Q func getJoins[Q dialect.Joinable]() joins[Q] { return joins[Q]{ + OauthTokens: buildJoinSet[oauthTokenJoins[Q]](OauthTokens.Columns, buildOauthTokenJoins), Organizations: buildJoinSet[organizationJoins[Q]](Organizations.Columns, buildOrganizationJoins), Users: buildJoinSet[userJoins[Q]](Users.Columns, buildUserJoins), } diff --git a/models/bob_loaders.bob.go b/models/bob_loaders.bob.go index 13aaec41..8d4df060 100644 --- a/models/bob_loaders.bob.go +++ b/models/bob_loaders.bob.go @@ -17,12 +17,14 @@ import ( var Preload = getPreloaders() type preloaders struct { + OauthToken oauthTokenPreloader Organization organizationPreloader User userPreloader } func getPreloaders() preloaders { return preloaders{ + OauthToken: buildOauthTokenPreloader(), Organization: buildOrganizationPreloader(), User: buildUserPreloader(), } @@ -35,12 +37,14 @@ var ( ) type thenLoaders[Q orm.Loadable] struct { + OauthToken oauthTokenThenLoader[Q] Organization organizationThenLoader[Q] User userThenLoader[Q] } func getThenLoaders[Q orm.Loadable]() thenLoaders[Q] { return thenLoaders[Q]{ + OauthToken: buildOauthTokenThenLoader[Q](), Organization: buildOrganizationThenLoader[Q](), User: buildUserThenLoader[Q](), } diff --git a/models/bob_types.bob_test.go b/models/bob_types.bob_test.go index 0c8e0212..052ca1fb 100644 --- a/models/bob_types.bob_test.go +++ b/models/bob_types.bob_test.go @@ -17,6 +17,9 @@ var testDB bob.Transactor[bob.Tx] // Make sure the type GooseDBVersion runs hooks after queries var _ bob.HookableType = &GooseDBVersion{} +// Make sure the type OauthToken runs hooks after queries +var _ bob.HookableType = &OauthToken{} + // Make sure the type Organization runs hooks after queries var _ bob.HookableType = &Organization{} diff --git a/models/bob_where.bob.go b/models/bob_where.bob.go index 88e47b87..358ab651 100644 --- a/models/bob_where.bob.go +++ b/models/bob_where.bob.go @@ -18,17 +18,20 @@ var ( func Where[Q psql.Filterable]() struct { GooseDBVersions gooseDBVersionWhere[Q] + OauthTokens oauthTokenWhere[Q] Organizations organizationWhere[Q] Sessions sessionWhere[Q] Users userWhere[Q] } { return struct { GooseDBVersions gooseDBVersionWhere[Q] + OauthTokens oauthTokenWhere[Q] Organizations organizationWhere[Q] Sessions sessionWhere[Q] Users userWhere[Q] }{ GooseDBVersions: buildGooseDBVersionWhere[Q](GooseDBVersions.Columns), + OauthTokens: buildOauthTokenWhere[Q](OauthTokens.Columns), Organizations: buildOrganizationWhere[Q](Organizations.Columns), Sessions: buildSessionWhere[Q](Sessions.Columns), Users: buildUserWhere[Q](Users.Columns), diff --git a/models/oauth_token.bob.go b/models/oauth_token.bob.go new file mode 100644 index 00000000..a78f6083 --- /dev/null +++ b/models/oauth_token.bob.go @@ -0,0 +1,703 @@ +// 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" + "fmt" + "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" + "github.com/stephenafamo/bob/mods" + "github.com/stephenafamo/bob/orm" + "github.com/stephenafamo/bob/types/pgtypes" +) + +// OauthToken is an object representing the database table. +type OauthToken struct { + ID int32 `db:"id,pk" ` + AccessToken string `db:"access_token" ` + Expires time.Time `db:"expires" ` + RefreshToken string `db:"refresh_token" ` + Username string `db:"username" ` + UserID int32 `db:"user_id" ` + + R oauthTokenR `db:"-" ` +} + +// OauthTokenSlice is an alias for a slice of pointers to OauthToken. +// This should almost always be used instead of []*OauthToken. +type OauthTokenSlice []*OauthToken + +// OauthTokens contains methods to work with the oauth_token table +var OauthTokens = psql.NewTablex[*OauthToken, OauthTokenSlice, *OauthTokenSetter]("", "oauth_token", buildOauthTokenColumns("oauth_token")) + +// OauthTokensQuery is a query on the oauth_token table +type OauthTokensQuery = *psql.ViewQuery[*OauthToken, OauthTokenSlice] + +// oauthTokenR is where relationships are stored. +type oauthTokenR struct { + UserUser *User // oauth_token.oauth_token_user_id_fkey +} + +func buildOauthTokenColumns(alias string) oauthTokenColumns { + return oauthTokenColumns{ + ColumnsExpr: expr.NewColumnsExpr( + "id", "access_token", "expires", "refresh_token", "username", "user_id", + ).WithParent("oauth_token"), + tableAlias: alias, + ID: psql.Quote(alias, "id"), + AccessToken: psql.Quote(alias, "access_token"), + Expires: psql.Quote(alias, "expires"), + RefreshToken: psql.Quote(alias, "refresh_token"), + Username: psql.Quote(alias, "username"), + UserID: psql.Quote(alias, "user_id"), + } +} + +type oauthTokenColumns struct { + expr.ColumnsExpr + tableAlias string + ID psql.Expression + AccessToken psql.Expression + Expires psql.Expression + RefreshToken psql.Expression + Username psql.Expression + UserID psql.Expression +} + +func (c oauthTokenColumns) Alias() string { + return c.tableAlias +} + +func (oauthTokenColumns) AliasedAs(alias string) oauthTokenColumns { + return buildOauthTokenColumns(alias) +} + +// OauthTokenSetter is used for insert/upsert/update operations +// All values are optional, and do not have to be set +// Generated columns are not included +type OauthTokenSetter struct { + ID omit.Val[int32] `db:"id,pk" ` + AccessToken omit.Val[string] `db:"access_token" ` + Expires omit.Val[time.Time] `db:"expires" ` + RefreshToken omit.Val[string] `db:"refresh_token" ` + Username omit.Val[string] `db:"username" ` + UserID omit.Val[int32] `db:"user_id" ` +} + +func (s OauthTokenSetter) SetColumns() []string { + vals := make([]string, 0, 6) + if s.ID.IsValue() { + vals = append(vals, "id") + } + if s.AccessToken.IsValue() { + vals = append(vals, "access_token") + } + if s.Expires.IsValue() { + vals = append(vals, "expires") + } + if s.RefreshToken.IsValue() { + vals = append(vals, "refresh_token") + } + if s.Username.IsValue() { + vals = append(vals, "username") + } + if s.UserID.IsValue() { + vals = append(vals, "user_id") + } + return vals +} + +func (s OauthTokenSetter) Overwrite(t *OauthToken) { + if s.ID.IsValue() { + t.ID = s.ID.MustGet() + } + if s.AccessToken.IsValue() { + t.AccessToken = s.AccessToken.MustGet() + } + if s.Expires.IsValue() { + t.Expires = s.Expires.MustGet() + } + if s.RefreshToken.IsValue() { + t.RefreshToken = s.RefreshToken.MustGet() + } + if s.Username.IsValue() { + t.Username = s.Username.MustGet() + } + if s.UserID.IsValue() { + t.UserID = s.UserID.MustGet() + } +} + +func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return OauthTokens.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, 6) + if s.ID.IsValue() { + vals[0] = psql.Arg(s.ID.MustGet()) + } else { + vals[0] = psql.Raw("DEFAULT") + } + + if s.AccessToken.IsValue() { + vals[1] = psql.Arg(s.AccessToken.MustGet()) + } else { + vals[1] = psql.Raw("DEFAULT") + } + + if s.Expires.IsValue() { + vals[2] = psql.Arg(s.Expires.MustGet()) + } else { + vals[2] = psql.Raw("DEFAULT") + } + + if s.RefreshToken.IsValue() { + vals[3] = psql.Arg(s.RefreshToken.MustGet()) + } else { + vals[3] = psql.Raw("DEFAULT") + } + + if s.Username.IsValue() { + vals[4] = psql.Arg(s.Username.MustGet()) + } else { + vals[4] = psql.Raw("DEFAULT") + } + + if s.UserID.IsValue() { + vals[5] = psql.Arg(s.UserID.MustGet()) + } else { + vals[5] = psql.Raw("DEFAULT") + } + + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") + })) +} + +func (s OauthTokenSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return um.Set(s.Expressions()...) +} + +func (s OauthTokenSetter) Expressions(prefix ...string) []bob.Expression { + exprs := make([]bob.Expression, 0, 6) + + if s.ID.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "id")...), + psql.Arg(s.ID), + }}) + } + + if s.AccessToken.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "access_token")...), + psql.Arg(s.AccessToken), + }}) + } + + if s.Expires.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "expires")...), + psql.Arg(s.Expires), + }}) + } + + if s.RefreshToken.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "refresh_token")...), + psql.Arg(s.RefreshToken), + }}) + } + + if s.Username.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "username")...), + psql.Arg(s.Username), + }}) + } + + if s.UserID.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "user_id")...), + psql.Arg(s.UserID), + }}) + } + + return exprs +} + +// FindOauthToken retrieves a single record by primary key +// If cols is empty Find will return all columns. +func FindOauthToken(ctx context.Context, exec bob.Executor, IDPK int32, cols ...string) (*OauthToken, error) { + if len(cols) == 0 { + return OauthTokens.Query( + sm.Where(OauthTokens.Columns.ID.EQ(psql.Arg(IDPK))), + ).One(ctx, exec) + } + + return OauthTokens.Query( + sm.Where(OauthTokens.Columns.ID.EQ(psql.Arg(IDPK))), + sm.Columns(OauthTokens.Columns.Only(cols...)), + ).One(ctx, exec) +} + +// OauthTokenExists checks the presence of a single record by primary key +func OauthTokenExists(ctx context.Context, exec bob.Executor, IDPK int32) (bool, error) { + return OauthTokens.Query( + sm.Where(OauthTokens.Columns.ID.EQ(psql.Arg(IDPK))), + ).Exists(ctx, exec) +} + +// AfterQueryHook is called after OauthToken is retrieved from the database +func (o *OauthToken) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = OauthTokens.AfterSelectHooks.RunHooks(ctx, exec, OauthTokenSlice{o}) + case bob.QueryTypeInsert: + ctx, err = OauthTokens.AfterInsertHooks.RunHooks(ctx, exec, OauthTokenSlice{o}) + case bob.QueryTypeUpdate: + ctx, err = OauthTokens.AfterUpdateHooks.RunHooks(ctx, exec, OauthTokenSlice{o}) + case bob.QueryTypeDelete: + ctx, err = OauthTokens.AfterDeleteHooks.RunHooks(ctx, exec, OauthTokenSlice{o}) + } + + return err +} + +// primaryKeyVals returns the primary key values of the OauthToken +func (o *OauthToken) primaryKeyVals() bob.Expression { + return psql.Arg(o.ID) +} + +func (o *OauthToken) pkEQ() dialect.Expression { + return psql.Quote("oauth_token", "id").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 OauthToken +func (o *OauthToken) Update(ctx context.Context, exec bob.Executor, s *OauthTokenSetter) error { + v, err := OauthTokens.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec) + if err != nil { + return err + } + + o.R = v.R + *o = *v + + return nil +} + +// Delete deletes a single OauthToken record with an executor +func (o *OauthToken) Delete(ctx context.Context, exec bob.Executor) error { + _, err := OauthTokens.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec) + return err +} + +// Reload refreshes the OauthToken using the executor +func (o *OauthToken) Reload(ctx context.Context, exec bob.Executor) error { + o2, err := OauthTokens.Query( + sm.Where(OauthTokens.Columns.ID.EQ(psql.Arg(o.ID))), + ).One(ctx, exec) + if err != nil { + return err + } + o2.R = o.R + *o = *o2 + + return nil +} + +// AfterQueryHook is called after OauthTokenSlice is retrieved from the database +func (o OauthTokenSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = OauthTokens.AfterSelectHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeInsert: + ctx, err = OauthTokens.AfterInsertHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeUpdate: + ctx, err = OauthTokens.AfterUpdateHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeDelete: + ctx, err = OauthTokens.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err +} + +func (o OauthTokenSlice) pkIN() dialect.Expression { + if len(o) == 0 { + return psql.Raw("NULL") + } + + return psql.Quote("oauth_token", "id").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 OauthTokenSlice) copyMatchingRows(from ...*OauthToken) { + for i, old := range o { + for _, new := range from { + if new.ID != old.ID { + continue + } + new.R = old.R + o[i] = new + break + } + } +} + +// UpdateMod modifies an update query with "WHERE primary_key IN (o...)" +func (o OauthTokenSlice) 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 OauthTokens.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 *OauthToken: + o.copyMatchingRows(retrieved) + case []*OauthToken: + o.copyMatchingRows(retrieved...) + case OauthTokenSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a OauthToken or a slice of OauthToken + // then run the AfterUpdateHooks on the slice + _, err = OauthTokens.AfterUpdateHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)" +func (o OauthTokenSlice) 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 OauthTokens.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 *OauthToken: + o.copyMatchingRows(retrieved) + case []*OauthToken: + o.copyMatchingRows(retrieved...) + case OauthTokenSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a OauthToken or a slice of OauthToken + // then run the AfterDeleteHooks on the slice + _, err = OauthTokens.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +func (o OauthTokenSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals OauthTokenSetter) error { + if len(o) == 0 { + return nil + } + + _, err := OauthTokens.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec) + return err +} + +func (o OauthTokenSlice) DeleteAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + _, err := OauthTokens.Delete(o.DeleteMod()).Exec(ctx, exec) + return err +} + +func (o OauthTokenSlice) ReloadAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + o2, err := OauthTokens.Query(sm.Where(o.pkIN())).All(ctx, exec) + if err != nil { + return err + } + + o.copyMatchingRows(o2...) + + return nil +} + +// UserUser starts a query for related objects on user_ +func (o *OauthToken) UserUser(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery { + return Users.Query(append(mods, + sm.Where(Users.Columns.ID.EQ(psql.Arg(o.UserID))), + )...) +} + +func (os OauthTokenSlice) UserUser(mods ...bob.Mod[*dialect.SelectQuery]) UsersQuery { + pkUserID := make(pgtypes.Array[int32], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkUserID = append(pkUserID, o.UserID) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkUserID), "integer[]")), + )) + + return Users.Query(append(mods, + sm.Where(psql.Group(Users.Columns.ID).OP("IN", PKArgExpr)), + )...) +} + +func attachOauthTokenUserUser0(ctx context.Context, exec bob.Executor, count int, oauthToken0 *OauthToken, user1 *User) (*OauthToken, error) { + setter := &OauthTokenSetter{ + UserID: omit.From(user1.ID), + } + + err := oauthToken0.Update(ctx, exec, setter) + if err != nil { + return nil, fmt.Errorf("attachOauthTokenUserUser0: %w", err) + } + + return oauthToken0, nil +} + +func (oauthToken0 *OauthToken) InsertUserUser(ctx context.Context, exec bob.Executor, related *UserSetter) error { + var err error + + user1, err := Users.Insert(related).One(ctx, exec) + if err != nil { + return fmt.Errorf("inserting related objects: %w", err) + } + + _, err = attachOauthTokenUserUser0(ctx, exec, 1, oauthToken0, user1) + if err != nil { + return err + } + + oauthToken0.R.UserUser = user1 + + user1.R.UserOauthTokens = append(user1.R.UserOauthTokens, oauthToken0) + + return nil +} + +func (oauthToken0 *OauthToken) AttachUserUser(ctx context.Context, exec bob.Executor, user1 *User) error { + var err error + + _, err = attachOauthTokenUserUser0(ctx, exec, 1, oauthToken0, user1) + if err != nil { + return err + } + + oauthToken0.R.UserUser = user1 + + user1.R.UserOauthTokens = append(user1.R.UserOauthTokens, oauthToken0) + + return nil +} + +type oauthTokenWhere[Q psql.Filterable] struct { + ID psql.WhereMod[Q, int32] + AccessToken psql.WhereMod[Q, string] + Expires psql.WhereMod[Q, time.Time] + RefreshToken psql.WhereMod[Q, string] + Username psql.WhereMod[Q, string] + UserID psql.WhereMod[Q, int32] +} + +func (oauthTokenWhere[Q]) AliasedAs(alias string) oauthTokenWhere[Q] { + return buildOauthTokenWhere[Q](buildOauthTokenColumns(alias)) +} + +func buildOauthTokenWhere[Q psql.Filterable](cols oauthTokenColumns) oauthTokenWhere[Q] { + return oauthTokenWhere[Q]{ + ID: psql.Where[Q, int32](cols.ID), + AccessToken: psql.Where[Q, string](cols.AccessToken), + Expires: psql.Where[Q, time.Time](cols.Expires), + RefreshToken: psql.Where[Q, string](cols.RefreshToken), + Username: psql.Where[Q, string](cols.Username), + UserID: psql.Where[Q, int32](cols.UserID), + } +} + +func (o *OauthToken) Preload(name string, retrieved any) error { + if o == nil { + return nil + } + + switch name { + case "UserUser": + rel, ok := retrieved.(*User) + if !ok { + return fmt.Errorf("oauthToken cannot load %T as %q", retrieved, name) + } + + o.R.UserUser = rel + + if rel != nil { + rel.R.UserOauthTokens = OauthTokenSlice{o} + } + return nil + default: + return fmt.Errorf("oauthToken has no relationship %q", name) + } +} + +type oauthTokenPreloader struct { + UserUser func(...psql.PreloadOption) psql.Preloader +} + +func buildOauthTokenPreloader() oauthTokenPreloader { + return oauthTokenPreloader{ + UserUser: func(opts ...psql.PreloadOption) psql.Preloader { + return psql.Preload[*User, UserSlice](psql.PreloadRel{ + Name: "UserUser", + Sides: []psql.PreloadSide{ + { + From: OauthTokens, + To: Users, + FromColumns: []string{"user_id"}, + ToColumns: []string{"id"}, + }, + }, + }, Users.Columns.Names(), opts...) + }, + } +} + +type oauthTokenThenLoader[Q orm.Loadable] struct { + UserUser func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] +} + +func buildOauthTokenThenLoader[Q orm.Loadable]() oauthTokenThenLoader[Q] { + type UserUserLoadInterface interface { + LoadUserUser(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } + + return oauthTokenThenLoader[Q]{ + UserUser: thenLoadBuilder[Q]( + "UserUser", + func(ctx context.Context, exec bob.Executor, retrieved UserUserLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadUserUser(ctx, exec, mods...) + }, + ), + } +} + +// LoadUserUser loads the oauthToken's UserUser into the .R struct +func (o *OauthToken) LoadUserUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.UserUser = nil + + related, err := o.UserUser(mods...).One(ctx, exec) + if err != nil { + return err + } + + related.R.UserOauthTokens = OauthTokenSlice{o} + + o.R.UserUser = related + return nil +} + +// LoadUserUser loads the oauthToken's UserUser into the .R struct +func (os OauthTokenSlice) LoadUserUser(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + users, err := os.UserUser(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range users { + + if !(o.UserID == rel.ID) { + continue + } + + rel.R.UserOauthTokens = append(rel.R.UserOauthTokens, o) + + o.R.UserUser = rel + break + } + } + + return nil +} + +type oauthTokenJoins[Q dialect.Joinable] struct { + typ string + UserUser modAs[Q, userColumns] +} + +func (j oauthTokenJoins[Q]) aliasedAs(alias string) oauthTokenJoins[Q] { + return buildOauthTokenJoins[Q](buildOauthTokenColumns(alias), j.typ) +} + +func buildOauthTokenJoins[Q dialect.Joinable](cols oauthTokenColumns, typ string) oauthTokenJoins[Q] { + return oauthTokenJoins[Q]{ + typ: typ, + UserUser: modAs[Q, userColumns]{ + c: Users.Columns, + f: func(to userColumns) bob.Mod[Q] { + mods := make(mods.QueryMods[Q], 0, 1) + + { + mods = append(mods, dialect.Join[Q](typ, Users.Name().As(to.Alias())).On( + to.ID.EQ(cols.UserID), + )) + } + + return mods + }, + }, + } +} diff --git a/models/user_.bob.go b/models/user_.bob.go index 2845012d..32bbc3bb 100644 --- a/models/user_.bob.go +++ b/models/user_.bob.go @@ -55,7 +55,8 @@ type UsersQuery = *psql.ViewQuery[*User, UserSlice] // userR is where relationships are stored. type userR struct { - Organization *Organization // user_.user__organization_id_fkey + UserOauthTokens OauthTokenSlice // oauth_token.oauth_token_user_id_fkey + Organization *Organization // user_.user__organization_id_fkey } func buildUserColumns(alias string) userColumns { @@ -602,6 +603,30 @@ func (o UserSlice) ReloadAll(ctx context.Context, exec bob.Executor) error { return nil } +// UserOauthTokens starts a query for related objects on oauth_token +func (o *User) UserOauthTokens(mods ...bob.Mod[*dialect.SelectQuery]) OauthTokensQuery { + return OauthTokens.Query(append(mods, + sm.Where(OauthTokens.Columns.UserID.EQ(psql.Arg(o.ID))), + )...) +} + +func (os UserSlice) UserOauthTokens(mods ...bob.Mod[*dialect.SelectQuery]) OauthTokensQuery { + pkID := make(pgtypes.Array[int32], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkID = append(pkID, o.ID) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkID), "integer[]")), + )) + + return OauthTokens.Query(append(mods, + sm.Where(psql.Group(OauthTokens.Columns.UserID).OP("IN", PKArgExpr)), + )...) +} + // Organization starts a query for related objects on organization func (o *User) Organization(mods ...bob.Mod[*dialect.SelectQuery]) OrganizationsQuery { return Organizations.Query(append(mods, @@ -626,6 +651,74 @@ func (os UserSlice) Organization(mods ...bob.Mod[*dialect.SelectQuery]) Organiza )...) } +func insertUserUserOauthTokens0(ctx context.Context, exec bob.Executor, oauthTokens1 []*OauthTokenSetter, user0 *User) (OauthTokenSlice, error) { + for i := range oauthTokens1 { + oauthTokens1[i].UserID = omit.From(user0.ID) + } + + ret, err := OauthTokens.Insert(bob.ToMods(oauthTokens1...)).All(ctx, exec) + if err != nil { + return ret, fmt.Errorf("insertUserUserOauthTokens0: %w", err) + } + + return ret, nil +} + +func attachUserUserOauthTokens0(ctx context.Context, exec bob.Executor, count int, oauthTokens1 OauthTokenSlice, user0 *User) (OauthTokenSlice, error) { + setter := &OauthTokenSetter{ + UserID: omit.From(user0.ID), + } + + err := oauthTokens1.UpdateAll(ctx, exec, *setter) + if err != nil { + return nil, fmt.Errorf("attachUserUserOauthTokens0: %w", err) + } + + return oauthTokens1, nil +} + +func (user0 *User) InsertUserOauthTokens(ctx context.Context, exec bob.Executor, related ...*OauthTokenSetter) error { + if len(related) == 0 { + return nil + } + + var err error + + oauthTokens1, err := insertUserUserOauthTokens0(ctx, exec, related, user0) + if err != nil { + return err + } + + user0.R.UserOauthTokens = append(user0.R.UserOauthTokens, oauthTokens1...) + + for _, rel := range oauthTokens1 { + rel.R.UserUser = user0 + } + return nil +} + +func (user0 *User) AttachUserOauthTokens(ctx context.Context, exec bob.Executor, related ...*OauthToken) error { + if len(related) == 0 { + return nil + } + + var err error + oauthTokens1 := OauthTokenSlice(related) + + _, err = attachUserUserOauthTokens0(ctx, exec, len(related), oauthTokens1, user0) + if err != nil { + return err + } + + user0.R.UserOauthTokens = append(user0.R.UserOauthTokens, oauthTokens1...) + + for _, rel := range related { + rel.R.UserUser = user0 + } + + return nil +} + func attachUserOrganization0(ctx context.Context, exec bob.Executor, count int, user0 *User, organization1 *Organization) (*User, error) { setter := &UserSetter{ OrganizationID: omitnull.From(organization1.ID), @@ -716,6 +809,20 @@ func (o *User) Preload(name string, retrieved any) error { } switch name { + case "UserOauthTokens": + rels, ok := retrieved.(OauthTokenSlice) + if !ok { + return fmt.Errorf("user cannot load %T as %q", retrieved, name) + } + + o.R.UserOauthTokens = rels + + for _, rel := range rels { + if rel != nil { + rel.R.UserUser = o + } + } + return nil case "Organization": rel, ok := retrieved.(*Organization) if !ok { @@ -756,15 +863,25 @@ func buildUserPreloader() userPreloader { } type userThenLoader[Q orm.Loadable] struct { - Organization func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] + UserOauthTokens func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] + Organization func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] } func buildUserThenLoader[Q orm.Loadable]() userThenLoader[Q] { + type UserOauthTokensLoadInterface interface { + LoadUserOauthTokens(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type OrganizationLoadInterface interface { LoadOrganization(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } return userThenLoader[Q]{ + UserOauthTokens: thenLoadBuilder[Q]( + "UserOauthTokens", + func(ctx context.Context, exec bob.Executor, retrieved UserOauthTokensLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadUserOauthTokens(ctx, exec, mods...) + }, + ), Organization: thenLoadBuilder[Q]( "Organization", func(ctx context.Context, exec bob.Executor, retrieved OrganizationLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -774,6 +891,67 @@ func buildUserThenLoader[Q orm.Loadable]() userThenLoader[Q] { } } +// LoadUserOauthTokens loads the user's UserOauthTokens into the .R struct +func (o *User) LoadUserOauthTokens(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.UserOauthTokens = nil + + related, err := o.UserOauthTokens(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, rel := range related { + rel.R.UserUser = o + } + + o.R.UserOauthTokens = related + return nil +} + +// LoadUserOauthTokens loads the user's UserOauthTokens into the .R struct +func (os UserSlice) LoadUserOauthTokens(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + oauthTokens, err := os.UserOauthTokens(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + o.R.UserOauthTokens = nil + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range oauthTokens { + + if !(o.ID == rel.UserID) { + continue + } + + rel.R.UserUser = o + + o.R.UserOauthTokens = append(o.R.UserOauthTokens, rel) + } + } + + return nil +} + // LoadOrganization loads the user's Organization into the .R struct func (o *User) LoadOrganization(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { @@ -830,8 +1008,9 @@ func (os UserSlice) LoadOrganization(ctx context.Context, exec bob.Executor, mod } type userJoins[Q dialect.Joinable] struct { - typ string - Organization modAs[Q, organizationColumns] + typ string + UserOauthTokens modAs[Q, oauthTokenColumns] + Organization modAs[Q, organizationColumns] } func (j userJoins[Q]) aliasedAs(alias string) userJoins[Q] { @@ -841,6 +1020,20 @@ func (j userJoins[Q]) aliasedAs(alias string) userJoins[Q] { func buildUserJoins[Q dialect.Joinable](cols userColumns, typ string) userJoins[Q] { return userJoins[Q]{ typ: typ, + UserOauthTokens: modAs[Q, oauthTokenColumns]{ + c: OauthTokens.Columns, + f: func(to oauthTokenColumns) bob.Mod[Q] { + mods := make(mods.QueryMods[Q], 0, 1) + + { + mods = append(mods, dialect.Join[Q](typ, OauthTokens.Name().As(to.Alias())).On( + to.UserID.EQ(cols.ID), + )) + } + + return mods + }, + }, Organization: modAs[Q, organizationColumns]{ c: Organizations.Columns, f: func(to organizationColumns) bob.Mod[Q] { diff --git a/templates/dashboard.html b/templates/dashboard.html index 2c7340ec..17ea22d4 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -94,9 +94,9 @@
- +

You can disconnect your account at any time in settings