Track access token and refresh token expiry

Also make a bunch more progress on actually updating the tokens when we
need them updated.
This commit is contained in:
Eli Ribble 2025-11-07 05:46:41 +00:00
parent cf01c8c5c6
commit 109495b702
No known key found for this signature in database
11 changed files with 348 additions and 104 deletions

@ -1 +1 @@
Subproject commit a99b4a72b2bb3dcff642209f029eb4e7d746fa8d Subproject commit e47b350f9231a16c815b927947f9d718cec2d3fe

249
arcgis.go
View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"context" "context"
"crypto/rand" "crypto/rand"
"crypto/sha256" "crypto/sha256"
@ -12,8 +13,10 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/Gleipnir-Technology/arcgis-go" "github.com/Gleipnir-Technology/arcgis-go"
@ -24,13 +27,28 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
) )
var NewOAuthTokenChannel chan struct{}
var CodeVerifier string = "random_secure_string_min_43_chars_long_should_be_stored_in_session" var CodeVerifier string = "random_secure_string_min_43_chars_long_should_be_stored_in_session"
type ErrorResponse struct {
Error ErrorResponseContent `json:"error"`
}
type ErrorResponseContent struct {
Code int `json:"code"`
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
Message string `json:"message"`
Details []string `json:"details"`
}
type OAuthTokenResponse struct { type OAuthTokenResponse struct {
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"` RefreshToken string `json:"refresh_token"`
Username string `json:"username"` RefreshTokenExpiresIn int `json:"refresh_token_expires_in"`
SSL bool `json:"ssl"`
Username string `json:"username"`
} }
// Build the ArcGIS authorization URL with PKCE // Build the ArcGIS authorization URL with PKCE
@ -66,12 +84,13 @@ func generateCodeVerifier() string {
} }
// Find out what we can about this user // Find out what we can about this user
func updateArcgisUserData(ctx context.Context, user *models.User, access_token string, expires time.Time, refresh_token string) { func updateArcgisUserData(ctx context.Context, user *models.User, access_token string, access_token_expires time.Time, refresh_token string, refresh_token_expires time.Time) {
client := arcgis.NewArcGIS( client := arcgis.NewArcGIS(
arcgis.AuthenticatorOAuth{ arcgis.AuthenticatorOAuth{
AccessToken: access_token, AccessToken: access_token,
Expires: expires, AccessTokenExpires: access_token_expires,
RefreshToken: refresh_token, RefreshToken: refresh_token,
RefreshTokenExpires: refresh_token_expires,
}, },
) )
portal, err := client.PortalsSelf() portal, err := client.PortalsSelf()
@ -158,49 +177,25 @@ func handleOauthAccessCode(ctx context.Context, user *models.User, code string)
return fmt.Errorf("Failed to create request: %v", err) return fmt.Errorf("Failed to create request: %v", err)
} }
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client := http.Client{} token, err := handleTokenRequest(ctx, req)
slog.Info("POST", slog.String("url", baseURL))
resp, err := client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("Failed to do request: %v", err) return fmt.Errorf("Failed to exchange authorization code for token: %v", err)
} }
defer resp.Body.Close() accessExpires := futureUTCTimestamp(token.ExpiresIn)
bodyBytes, err := io.ReadAll(resp.Body) refreshExpires := futureUTCTimestamp(token.RefreshTokenExpiresIn)
slog.Info("Response", slog.Int("status", 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)
}
slog.Info("Oauth token acquired",
slog.String("refresh token", tokenResponse.RefreshToken),
slog.String("access token", tokenResponse.AccessToken),
slog.Int("expires", tokenResponse.ExpiresIn),
)
expires := futureUTCTimestamp(tokenResponse.ExpiresIn)
setter := models.OauthTokenSetter{ setter := models.OauthTokenSetter{
AccessToken: omit.From(tokenResponse.AccessToken), AccessToken: omit.From(token.AccessToken),
Expires: omit.From(expires), AccessTokenExpires: omit.From(accessExpires),
RefreshToken: omit.From(tokenResponse.RefreshToken), RefreshToken: omit.From(token.RefreshToken),
Username: omit.From(tokenResponse.Username), RefreshTokenExpires: omit.From(refreshExpires),
Username: omit.From(token.Username),
} }
err = user.InsertUserOauthTokens(ctx, PGInstance.BobDB, &setter) err = user.InsertUserOauthTokens(ctx, PGInstance.BobDB, &setter)
if err != nil { if err != nil {
return fmt.Errorf("Failed to save token to database: %v", err) return fmt.Errorf("Failed to save token to database: %v", err)
} }
go updateArcgisUserData(context.Background(), user, tokenResponse.AccessToken, expires, tokenResponse.RefreshToken) go updateArcgisUserData(context.Background(), user, token.AccessToken, accessExpires, token.RefreshToken, refreshExpires)
NewOAuthTokenChannel <- struct{}{}
return nil return nil
} }
@ -216,14 +211,174 @@ func redirectURL() string {
} }
// This is a goroutine that is in charge of getting Fieldseeker data and keeping it fresh. // This is a goroutine that is in charge of getting Fieldseeker data and keeping it fresh.
func refreshFieldseekerData(ctx context.Context, newOauthCh <-chan int) { func refreshFieldseekerData(ctx context.Context, newOauthCh <-chan struct{}) {
for { for {
workerCtx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
oauths, err := models.OauthTokens.Query().All(ctx, PGInstance.BobDB)
if err != nil {
slog.Error("Failed to get oauths", slog.String("err", err.Error()))
return
}
for _, oauth := range oauths {
wg.Add(1)
go func() {
defer wg.Done()
maintainOAuth(workerCtx, oauth)
}()
}
select { select {
case <-ctx.Done(): case <-ctx.Done():
slog.Info("Exiting refresh worker") slog.Info("Exiting refresh worker...")
cancel()
wg.Wait()
return return
case id := <-newOauthCh: case <-newOauthCh:
slog.Info("Adding oauth to background work", slog.Int("oauth id", id)) slog.Info("Updating oauth background work")
cancel()
wg.Wait()
} }
} }
} }
func maintainOAuth(ctx context.Context, oauth *models.OauthToken) {
refreshDelay := time.Until(oauth.AccessTokenExpires)
slog.Info("Need to refresh oauth", slog.Int("id", int(oauth.ID)), slog.Float64("seconds", refreshDelay.Seconds()))
if oauth.AccessTokenExpires.Before(time.Now()) {
err := refreshOAuth(ctx, oauth)
if err != nil {
slog.Error("Failed to refresh token", slog.String("err", err.Error()))
return
}
}
ticker := time.NewTicker(refreshDelay)
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func refreshOAuth(ctx context.Context, oauth *models.OauthToken) error {
baseURL := "https://www.arcgis.com/sharing/rest/oauth2/token/"
form := url.Values{
"grant_type": []string{"refresh_token"},
"client_id": []string{ClientID},
"refresh_token": []string{oauth.RefreshToken},
}
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")
token, err := handleTokenRequest(ctx, req)
if err != nil {
return fmt.Errorf("Failed to handle request: %v", err)
}
accessExpires := futureUTCTimestamp(token.ExpiresIn)
refreshExpires := futureUTCTimestamp(token.RefreshTokenExpiresIn)
setter := models.OauthTokenSetter{
AccessToken: omit.From(token.AccessToken),
AccessTokenExpires: omit.From(accessExpires),
RefreshToken: omit.From(token.RefreshToken),
RefreshTokenExpires: omit.From(refreshExpires),
Username: omit.From(token.Username),
}
err = oauth.Update(ctx, PGInstance.BobDB, &setter)
if err != nil {
return fmt.Errorf("Failed to update oauth in database: %v", err)
}
return nil
}
func handleTokenRequest(ctx context.Context, req *http.Request) (*OAuthTokenResponse, error) {
client := http.Client{}
slog.Info("POST", slog.String("url", req.URL.String()))
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("Failed to do request: %v", err)
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
slog.Info("Token request", slog.Int("status", resp.StatusCode))
saveResponse(bodyBytes, "token.json")
if resp.StatusCode >= http.StatusBadRequest {
if err != nil {
return nil, 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 nil, fmt.Errorf("API response JSON error: %d: %v", resp.StatusCode, errorResp)
}
return nil, fmt.Errorf("API returned error status %d: %s", resp.StatusCode, bodyString)
}
//logResponseHeaders(resp)
var tokenResponse OAuthTokenResponse
err = json.Unmarshal(bodyBytes, &tokenResponse)
if err != nil {
return nil, fmt.Errorf("Failed to unmarshal JSON: %v", err)
}
// Just because we got a 200-level status code doesn't mean it worked. Experience has taught us that
// we can get errors without anything indicated in the headers or the status code
if tokenResponse == (OAuthTokenResponse{}) {
var errorResponse ErrorResponse
err = json.Unmarshal(bodyBytes, &errorResponse)
if err != nil {
return nil, fmt.Errorf("Failed to unmarshal error JSON: %v", err)
}
if errorResponse.Error.Code > 0 {
return nil, errors.New(fmt.Sprintf("API error %d: %s: %s (%s)",
errorResponse.Error.Code,
errorResponse.Error.Error,
errorResponse.Error.ErrorDescription,
errorResponse.Error.Message,
))
}
}
slog.Info("Oauth token acquired",
slog.String("refresh token", tokenResponse.RefreshToken),
slog.String("access token", tokenResponse.AccessToken),
slog.Int("access expires", tokenResponse.ExpiresIn),
slog.Int("refresh expires", tokenResponse.RefreshTokenExpiresIn),
)
return &tokenResponse, nil
}
func logResponseHeaders(resp *http.Response) {
if resp == nil {
slog.Info("Response is nil")
return
}
slog.Info("HTTP Response headers",
"status", resp.Status,
"statusCode", resp.StatusCode)
for name, values := range resp.Header {
slog.Info("Header",
"name", name,
"values", values)
}
}
func saveResponse(data []byte, filename string) {
dest, err := os.Create(filename)
if err != nil {
slog.Error("Failed to create file", slog.String("filename", filename), slog.String("err", err.Error()))
return
}
_, err = io.Copy(dest, bytes.NewReader(data))
if err != nil {
slog.Error("Failed to write", slog.String("filename", filename), slog.String("err", err.Error()))
return
}
slog.Info("Wrote response", slog.String("filename", filename))
}

View file

@ -33,8 +33,8 @@ var OauthTokens = Table[
Generated: false, Generated: false,
AutoIncr: false, AutoIncr: false,
}, },
Expires: column{ AccessTokenExpires: column{
Name: "expires", Name: "access_token_expires",
DBType: "timestamp without time zone", DBType: "timestamp without time zone",
Default: "", Default: "",
Comment: "", Comment: "",
@ -87,6 +87,15 @@ var OauthTokens = Table[
Generated: false, Generated: false,
AutoIncr: false, AutoIncr: false,
}, },
RefreshTokenExpires: column{
Name: "refresh_token_expires",
DBType: "timestamp without time zone",
Default: "CURRENT_TIMESTAMP",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
}, },
Indexes: oauthTokenIndexes{ Indexes: oauthTokenIndexes{
OauthTokenPkey: index{ OauthTokenPkey: index{
@ -130,17 +139,18 @@ var OauthTokens = Table[
type oauthTokenColumns struct { type oauthTokenColumns struct {
ID column ID column
AccessToken column AccessToken column
Expires column AccessTokenExpires column
RefreshToken column RefreshToken column
Username column Username column
UserID column UserID column
ArcgisID column ArcgisID column
ArcgisLicenseTypeID column ArcgisLicenseTypeID column
RefreshTokenExpires column
} }
func (c oauthTokenColumns) AsSlice() []column { func (c oauthTokenColumns) AsSlice() []column {
return []column{ return []column{
c.ID, c.AccessToken, c.Expires, c.RefreshToken, c.Username, c.UserID, c.ArcgisID, c.ArcgisLicenseTypeID, c.ID, c.AccessToken, c.AccessTokenExpires, c.RefreshToken, c.Username, c.UserID, c.ArcgisID, c.ArcgisLicenseTypeID, c.RefreshTokenExpires,
} }
} }

View file

@ -72,12 +72,13 @@ func (f *Factory) FromExistingOauthToken(m *models.OauthToken) *OauthTokenTempla
o.ID = func() int32 { return m.ID } o.ID = func() int32 { return m.ID }
o.AccessToken = func() string { return m.AccessToken } o.AccessToken = func() string { return m.AccessToken }
o.Expires = func() time.Time { return m.Expires } o.AccessTokenExpires = func() time.Time { return m.AccessTokenExpires }
o.RefreshToken = func() string { return m.RefreshToken } o.RefreshToken = func() string { return m.RefreshToken }
o.Username = func() string { return m.Username } o.Username = func() string { return m.Username }
o.UserID = func() int32 { return m.UserID } o.UserID = func() int32 { return m.UserID }
o.ArcgisID = func() null.Val[string] { return m.ArcgisID } o.ArcgisID = func() null.Val[string] { return m.ArcgisID }
o.ArcgisLicenseTypeID = func() null.Val[string] { return m.ArcgisLicenseTypeID } o.ArcgisLicenseTypeID = func() null.Val[string] { return m.ArcgisLicenseTypeID }
o.RefreshTokenExpires = func() time.Time { return m.RefreshTokenExpires }
ctx := context.Background() ctx := context.Background()
if m.R.UserUser != nil { if m.R.UserUser != nil {

View file

@ -39,12 +39,13 @@ func (mods OauthTokenModSlice) Apply(ctx context.Context, n *OauthTokenTemplate)
type OauthTokenTemplate struct { type OauthTokenTemplate struct {
ID func() int32 ID func() int32
AccessToken func() string AccessToken func() string
Expires func() time.Time AccessTokenExpires func() time.Time
RefreshToken func() string RefreshToken func() string
Username func() string Username func() string
UserID func() int32 UserID func() int32
ArcgisID func() null.Val[string] ArcgisID func() null.Val[string]
ArcgisLicenseTypeID func() null.Val[string] ArcgisLicenseTypeID func() null.Val[string]
RefreshTokenExpires func() time.Time
r oauthTokenR r oauthTokenR
f *Factory f *Factory
@ -91,9 +92,9 @@ func (o OauthTokenTemplate) BuildSetter() *models.OauthTokenSetter {
val := o.AccessToken() val := o.AccessToken()
m.AccessToken = omit.From(val) m.AccessToken = omit.From(val)
} }
if o.Expires != nil { if o.AccessTokenExpires != nil {
val := o.Expires() val := o.AccessTokenExpires()
m.Expires = omit.From(val) m.AccessTokenExpires = omit.From(val)
} }
if o.RefreshToken != nil { if o.RefreshToken != nil {
val := o.RefreshToken() val := o.RefreshToken()
@ -115,6 +116,10 @@ func (o OauthTokenTemplate) BuildSetter() *models.OauthTokenSetter {
val := o.ArcgisLicenseTypeID() val := o.ArcgisLicenseTypeID()
m.ArcgisLicenseTypeID = omitnull.FromNull(val) m.ArcgisLicenseTypeID = omitnull.FromNull(val)
} }
if o.RefreshTokenExpires != nil {
val := o.RefreshTokenExpires()
m.RefreshTokenExpires = omit.From(val)
}
return m return m
} }
@ -143,8 +148,8 @@ func (o OauthTokenTemplate) Build() *models.OauthToken {
if o.AccessToken != nil { if o.AccessToken != nil {
m.AccessToken = o.AccessToken() m.AccessToken = o.AccessToken()
} }
if o.Expires != nil { if o.AccessTokenExpires != nil {
m.Expires = o.Expires() m.AccessTokenExpires = o.AccessTokenExpires()
} }
if o.RefreshToken != nil { if o.RefreshToken != nil {
m.RefreshToken = o.RefreshToken() m.RefreshToken = o.RefreshToken()
@ -161,6 +166,9 @@ func (o OauthTokenTemplate) Build() *models.OauthToken {
if o.ArcgisLicenseTypeID != nil { if o.ArcgisLicenseTypeID != nil {
m.ArcgisLicenseTypeID = o.ArcgisLicenseTypeID() m.ArcgisLicenseTypeID = o.ArcgisLicenseTypeID()
} }
if o.RefreshTokenExpires != nil {
m.RefreshTokenExpires = o.RefreshTokenExpires()
}
o.setModelRels(m) o.setModelRels(m)
@ -185,9 +193,9 @@ func ensureCreatableOauthToken(m *models.OauthTokenSetter) {
val := random_string(nil) val := random_string(nil)
m.AccessToken = omit.From(val) m.AccessToken = omit.From(val)
} }
if !(m.Expires.IsValue()) { if !(m.AccessTokenExpires.IsValue()) {
val := random_time_Time(nil) val := random_time_Time(nil)
m.Expires = omit.From(val) m.AccessTokenExpires = omit.From(val)
} }
if !(m.RefreshToken.IsValue()) { if !(m.RefreshToken.IsValue()) {
val := random_string(nil) val := random_string(nil)
@ -322,12 +330,13 @@ func (m oauthTokenMods) RandomizeAllColumns(f *faker.Faker) OauthTokenMod {
return OauthTokenModSlice{ return OauthTokenModSlice{
OauthTokenMods.RandomID(f), OauthTokenMods.RandomID(f),
OauthTokenMods.RandomAccessToken(f), OauthTokenMods.RandomAccessToken(f),
OauthTokenMods.RandomExpires(f), OauthTokenMods.RandomAccessTokenExpires(f),
OauthTokenMods.RandomRefreshToken(f), OauthTokenMods.RandomRefreshToken(f),
OauthTokenMods.RandomUsername(f), OauthTokenMods.RandomUsername(f),
OauthTokenMods.RandomUserID(f), OauthTokenMods.RandomUserID(f),
OauthTokenMods.RandomArcgisID(f), OauthTokenMods.RandomArcgisID(f),
OauthTokenMods.RandomArcgisLicenseTypeID(f), OauthTokenMods.RandomArcgisLicenseTypeID(f),
OauthTokenMods.RandomRefreshTokenExpires(f),
} }
} }
@ -394,31 +403,31 @@ func (m oauthTokenMods) RandomAccessToken(f *faker.Faker) OauthTokenMod {
} }
// Set the model columns to this value // Set the model columns to this value
func (m oauthTokenMods) Expires(val time.Time) OauthTokenMod { func (m oauthTokenMods) AccessTokenExpires(val time.Time) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Expires = func() time.Time { return val } o.AccessTokenExpires = func() time.Time { return val }
}) })
} }
// Set the Column from the function // Set the Column from the function
func (m oauthTokenMods) ExpiresFunc(f func() time.Time) OauthTokenMod { func (m oauthTokenMods) AccessTokenExpiresFunc(f func() time.Time) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Expires = f o.AccessTokenExpires = f
}) })
} }
// Clear any values for the column // Clear any values for the column
func (m oauthTokenMods) UnsetExpires() OauthTokenMod { func (m oauthTokenMods) UnsetAccessTokenExpires() OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Expires = nil o.AccessTokenExpires = nil
}) })
} }
// Generates a random value for the column using the given faker // Generates a random value for the column using the given faker
// if faker is nil, a default faker is used // if faker is nil, a default faker is used
func (m oauthTokenMods) RandomExpires(f *faker.Faker) OauthTokenMod { func (m oauthTokenMods) RandomAccessTokenExpires(f *faker.Faker) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) { return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Expires = func() time.Time { o.AccessTokenExpires = func() time.Time {
return random_time_Time(f) return random_time_Time(f)
} }
}) })
@ -623,6 +632,37 @@ func (m oauthTokenMods) RandomArcgisLicenseTypeIDNotNull(f *faker.Faker) OauthTo
}) })
} }
// Set the model columns to this value
func (m oauthTokenMods) RefreshTokenExpires(val time.Time) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.RefreshTokenExpires = func() time.Time { return val }
})
}
// Set the Column from the function
func (m oauthTokenMods) RefreshTokenExpiresFunc(f func() time.Time) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.RefreshTokenExpires = f
})
}
// Clear any values for the column
func (m oauthTokenMods) UnsetRefreshTokenExpires() OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.RefreshTokenExpires = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m oauthTokenMods) RandomRefreshTokenExpires(f *faker.Faker) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.RefreshTokenExpires = func() time.Time {
return random_time_Time(f)
}
})
}
func (m oauthTokenMods) WithParentsCascading() OauthTokenMod { func (m oauthTokenMods) WithParentsCascading() OauthTokenMod {
return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) { return OauthTokenModFunc(func(ctx context.Context, o *OauthTokenTemplate) {
if isDone, _ := oauthTokenWithParentsCascadingCtx.Value(ctx); isDone { if isDone, _ := oauthTokenWithParentsCascadingCtx.Value(ctx); isDone {

View file

@ -82,14 +82,14 @@ func main() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
newTokenChannel := make(chan int, 10) NewOAuthTokenChannel = make(chan struct{}, 10)
var waitGroup sync.WaitGroup var waitGroup sync.WaitGroup
waitGroup.Add(1) waitGroup.Add(1)
go func() { go func() {
defer waitGroup.Done() defer waitGroup.Done()
refreshFieldseekerData(ctx, newTokenChannel) refreshFieldseekerData(ctx, NewOAuthTokenChannel)
}() }()
server := &http.Server{ server := &http.Server{

View file

@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE oauth_token RENAME COLUMN expires TO access_token_expires;
ALTER TABLE oauth_token ADD COLUMN refresh_token_expires TIMESTAMP NOT NULL DEFAULT current_timestamp;
-- +goose Down
ALTER TABLE oauth_token DROP COLUMN refresh_token_expires;
ALTER TABLE oauth_token RENAME COLUMN access_token_expires TO expires;

View file

@ -28,12 +28,13 @@ import (
type OauthToken struct { type OauthToken struct {
ID int32 `db:"id,pk" ` ID int32 `db:"id,pk" `
AccessToken string `db:"access_token" ` AccessToken string `db:"access_token" `
Expires time.Time `db:"expires" ` AccessTokenExpires time.Time `db:"access_token_expires" `
RefreshToken string `db:"refresh_token" ` RefreshToken string `db:"refresh_token" `
Username string `db:"username" ` Username string `db:"username" `
UserID int32 `db:"user_id" ` UserID int32 `db:"user_id" `
ArcgisID null.Val[string] `db:"arcgis_id" ` ArcgisID null.Val[string] `db:"arcgis_id" `
ArcgisLicenseTypeID null.Val[string] `db:"arcgis_license_type_id" ` ArcgisLicenseTypeID null.Val[string] `db:"arcgis_license_type_id" `
RefreshTokenExpires time.Time `db:"refresh_token_expires" `
R oauthTokenR `db:"-" ` R oauthTokenR `db:"-" `
} }
@ -56,17 +57,18 @@ type oauthTokenR struct {
func buildOauthTokenColumns(alias string) oauthTokenColumns { func buildOauthTokenColumns(alias string) oauthTokenColumns {
return oauthTokenColumns{ return oauthTokenColumns{
ColumnsExpr: expr.NewColumnsExpr( ColumnsExpr: expr.NewColumnsExpr(
"id", "access_token", "expires", "refresh_token", "username", "user_id", "arcgis_id", "arcgis_license_type_id", "id", "access_token", "access_token_expires", "refresh_token", "username", "user_id", "arcgis_id", "arcgis_license_type_id", "refresh_token_expires",
).WithParent("oauth_token"), ).WithParent("oauth_token"),
tableAlias: alias, tableAlias: alias,
ID: psql.Quote(alias, "id"), ID: psql.Quote(alias, "id"),
AccessToken: psql.Quote(alias, "access_token"), AccessToken: psql.Quote(alias, "access_token"),
Expires: psql.Quote(alias, "expires"), AccessTokenExpires: psql.Quote(alias, "access_token_expires"),
RefreshToken: psql.Quote(alias, "refresh_token"), RefreshToken: psql.Quote(alias, "refresh_token"),
Username: psql.Quote(alias, "username"), Username: psql.Quote(alias, "username"),
UserID: psql.Quote(alias, "user_id"), UserID: psql.Quote(alias, "user_id"),
ArcgisID: psql.Quote(alias, "arcgis_id"), ArcgisID: psql.Quote(alias, "arcgis_id"),
ArcgisLicenseTypeID: psql.Quote(alias, "arcgis_license_type_id"), ArcgisLicenseTypeID: psql.Quote(alias, "arcgis_license_type_id"),
RefreshTokenExpires: psql.Quote(alias, "refresh_token_expires"),
} }
} }
@ -75,12 +77,13 @@ type oauthTokenColumns struct {
tableAlias string tableAlias string
ID psql.Expression ID psql.Expression
AccessToken psql.Expression AccessToken psql.Expression
Expires psql.Expression AccessTokenExpires psql.Expression
RefreshToken psql.Expression RefreshToken psql.Expression
Username psql.Expression Username psql.Expression
UserID psql.Expression UserID psql.Expression
ArcgisID psql.Expression ArcgisID psql.Expression
ArcgisLicenseTypeID psql.Expression ArcgisLicenseTypeID psql.Expression
RefreshTokenExpires psql.Expression
} }
func (c oauthTokenColumns) Alias() string { func (c oauthTokenColumns) Alias() string {
@ -97,24 +100,25 @@ func (oauthTokenColumns) AliasedAs(alias string) oauthTokenColumns {
type OauthTokenSetter struct { type OauthTokenSetter struct {
ID omit.Val[int32] `db:"id,pk" ` ID omit.Val[int32] `db:"id,pk" `
AccessToken omit.Val[string] `db:"access_token" ` AccessToken omit.Val[string] `db:"access_token" `
Expires omit.Val[time.Time] `db:"expires" ` AccessTokenExpires omit.Val[time.Time] `db:"access_token_expires" `
RefreshToken omit.Val[string] `db:"refresh_token" ` RefreshToken omit.Val[string] `db:"refresh_token" `
Username omit.Val[string] `db:"username" ` Username omit.Val[string] `db:"username" `
UserID omit.Val[int32] `db:"user_id" ` UserID omit.Val[int32] `db:"user_id" `
ArcgisID omitnull.Val[string] `db:"arcgis_id" ` ArcgisID omitnull.Val[string] `db:"arcgis_id" `
ArcgisLicenseTypeID omitnull.Val[string] `db:"arcgis_license_type_id" ` ArcgisLicenseTypeID omitnull.Val[string] `db:"arcgis_license_type_id" `
RefreshTokenExpires omit.Val[time.Time] `db:"refresh_token_expires" `
} }
func (s OauthTokenSetter) SetColumns() []string { func (s OauthTokenSetter) SetColumns() []string {
vals := make([]string, 0, 8) vals := make([]string, 0, 9)
if s.ID.IsValue() { if s.ID.IsValue() {
vals = append(vals, "id") vals = append(vals, "id")
} }
if s.AccessToken.IsValue() { if s.AccessToken.IsValue() {
vals = append(vals, "access_token") vals = append(vals, "access_token")
} }
if s.Expires.IsValue() { if s.AccessTokenExpires.IsValue() {
vals = append(vals, "expires") vals = append(vals, "access_token_expires")
} }
if s.RefreshToken.IsValue() { if s.RefreshToken.IsValue() {
vals = append(vals, "refresh_token") vals = append(vals, "refresh_token")
@ -131,6 +135,9 @@ func (s OauthTokenSetter) SetColumns() []string {
if !s.ArcgisLicenseTypeID.IsUnset() { if !s.ArcgisLicenseTypeID.IsUnset() {
vals = append(vals, "arcgis_license_type_id") vals = append(vals, "arcgis_license_type_id")
} }
if s.RefreshTokenExpires.IsValue() {
vals = append(vals, "refresh_token_expires")
}
return vals return vals
} }
@ -141,8 +148,8 @@ func (s OauthTokenSetter) Overwrite(t *OauthToken) {
if s.AccessToken.IsValue() { if s.AccessToken.IsValue() {
t.AccessToken = s.AccessToken.MustGet() t.AccessToken = s.AccessToken.MustGet()
} }
if s.Expires.IsValue() { if s.AccessTokenExpires.IsValue() {
t.Expires = s.Expires.MustGet() t.AccessTokenExpires = s.AccessTokenExpires.MustGet()
} }
if s.RefreshToken.IsValue() { if s.RefreshToken.IsValue() {
t.RefreshToken = s.RefreshToken.MustGet() t.RefreshToken = s.RefreshToken.MustGet()
@ -159,6 +166,9 @@ func (s OauthTokenSetter) Overwrite(t *OauthToken) {
if !s.ArcgisLicenseTypeID.IsUnset() { if !s.ArcgisLicenseTypeID.IsUnset() {
t.ArcgisLicenseTypeID = s.ArcgisLicenseTypeID.MustGetNull() t.ArcgisLicenseTypeID = s.ArcgisLicenseTypeID.MustGetNull()
} }
if s.RefreshTokenExpires.IsValue() {
t.RefreshTokenExpires = s.RefreshTokenExpires.MustGet()
}
} }
func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) { func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
@ -167,7 +177,7 @@ func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
}) })
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) { q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 8) vals := make([]bob.Expression, 9)
if s.ID.IsValue() { if s.ID.IsValue() {
vals[0] = psql.Arg(s.ID.MustGet()) vals[0] = psql.Arg(s.ID.MustGet())
} else { } else {
@ -180,8 +190,8 @@ func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
vals[1] = psql.Raw("DEFAULT") vals[1] = psql.Raw("DEFAULT")
} }
if s.Expires.IsValue() { if s.AccessTokenExpires.IsValue() {
vals[2] = psql.Arg(s.Expires.MustGet()) vals[2] = psql.Arg(s.AccessTokenExpires.MustGet())
} else { } else {
vals[2] = psql.Raw("DEFAULT") vals[2] = psql.Raw("DEFAULT")
} }
@ -216,6 +226,12 @@ func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
vals[7] = psql.Raw("DEFAULT") vals[7] = psql.Raw("DEFAULT")
} }
if s.RefreshTokenExpires.IsValue() {
vals[8] = psql.Arg(s.RefreshTokenExpires.MustGet())
} else {
vals[8] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
})) }))
} }
@ -225,7 +241,7 @@ func (s OauthTokenSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
} }
func (s OauthTokenSetter) Expressions(prefix ...string) []bob.Expression { func (s OauthTokenSetter) Expressions(prefix ...string) []bob.Expression {
exprs := make([]bob.Expression, 0, 8) exprs := make([]bob.Expression, 0, 9)
if s.ID.IsValue() { if s.ID.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
@ -241,10 +257,10 @@ func (s OauthTokenSetter) Expressions(prefix ...string) []bob.Expression {
}}) }})
} }
if s.Expires.IsValue() { if s.AccessTokenExpires.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "expires")...), psql.Quote(append(prefix, "access_token_expires")...),
psql.Arg(s.Expires), psql.Arg(s.AccessTokenExpires),
}}) }})
} }
@ -283,6 +299,13 @@ func (s OauthTokenSetter) Expressions(prefix ...string) []bob.Expression {
}}) }})
} }
if s.RefreshTokenExpires.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "refresh_token_expires")...),
psql.Arg(s.RefreshTokenExpires),
}})
}
return exprs return exprs
} }
@ -584,12 +607,13 @@ func (oauthToken0 *OauthToken) AttachUserUser(ctx context.Context, exec bob.Exec
type oauthTokenWhere[Q psql.Filterable] struct { type oauthTokenWhere[Q psql.Filterable] struct {
ID psql.WhereMod[Q, int32] ID psql.WhereMod[Q, int32]
AccessToken psql.WhereMod[Q, string] AccessToken psql.WhereMod[Q, string]
Expires psql.WhereMod[Q, time.Time] AccessTokenExpires psql.WhereMod[Q, time.Time]
RefreshToken psql.WhereMod[Q, string] RefreshToken psql.WhereMod[Q, string]
Username psql.WhereMod[Q, string] Username psql.WhereMod[Q, string]
UserID psql.WhereMod[Q, int32] UserID psql.WhereMod[Q, int32]
ArcgisID psql.WhereNullMod[Q, string] ArcgisID psql.WhereNullMod[Q, string]
ArcgisLicenseTypeID psql.WhereNullMod[Q, string] ArcgisLicenseTypeID psql.WhereNullMod[Q, string]
RefreshTokenExpires psql.WhereMod[Q, time.Time]
} }
func (oauthTokenWhere[Q]) AliasedAs(alias string) oauthTokenWhere[Q] { func (oauthTokenWhere[Q]) AliasedAs(alias string) oauthTokenWhere[Q] {
@ -600,12 +624,13 @@ func buildOauthTokenWhere[Q psql.Filterable](cols oauthTokenColumns) oauthTokenW
return oauthTokenWhere[Q]{ return oauthTokenWhere[Q]{
ID: psql.Where[Q, int32](cols.ID), ID: psql.Where[Q, int32](cols.ID),
AccessToken: psql.Where[Q, string](cols.AccessToken), AccessToken: psql.Where[Q, string](cols.AccessToken),
Expires: psql.Where[Q, time.Time](cols.Expires), AccessTokenExpires: psql.Where[Q, time.Time](cols.AccessTokenExpires),
RefreshToken: psql.Where[Q, string](cols.RefreshToken), RefreshToken: psql.Where[Q, string](cols.RefreshToken),
Username: psql.Where[Q, string](cols.Username), Username: psql.Where[Q, string](cols.Username),
UserID: psql.Where[Q, int32](cols.UserID), UserID: psql.Where[Q, int32](cols.UserID),
ArcgisID: psql.WhereNull[Q, string](cols.ArcgisID), ArcgisID: psql.WhereNull[Q, string](cols.ArcgisID),
ArcgisLicenseTypeID: psql.WhereNull[Q, string](cols.ArcgisLicenseTypeID), ArcgisLicenseTypeID: psql.WhereNull[Q, string](cols.ArcgisLicenseTypeID),
RefreshTokenExpires: psql.Where[Q, time.Time](cols.RefreshTokenExpires),
} }
} }

View file

@ -21,7 +21,7 @@ import (
//go:embed oauth_by_user_id.bob.sql //go:embed oauth_by_user_id.bob.sql
var formattedQueries_oauth_by_user_id string var formattedQueries_oauth_by_user_id string
var oauthTokenByUserIdSQL = formattedQueries_oauth_by_user_id[156:550] var oauthTokenByUserIdSQL = formattedQueries_oauth_by_user_id[156:642]
type OauthTokenByUserIdQuery = orm.ModQuery[*dialect.SelectQuery, oauthTokenByUserId, OauthTokenByUserIdRow, []OauthTokenByUserIdRow, oauthTokenByUserIdTransformer] type OauthTokenByUserIdQuery = orm.ModQuery[*dialect.SelectQuery, oauthTokenByUserId, OauthTokenByUserIdRow, []OauthTokenByUserIdRow, oauthTokenByUserIdTransformer]
@ -44,12 +44,13 @@ func OauthTokenByUserId(UserID int32) *OauthTokenByUserIdQuery {
var t OauthTokenByUserIdRow var t OauthTokenByUserIdRow
row.ScheduleScanByIndex(0, &t.ID) row.ScheduleScanByIndex(0, &t.ID)
row.ScheduleScanByIndex(1, &t.AccessToken) row.ScheduleScanByIndex(1, &t.AccessToken)
row.ScheduleScanByIndex(2, &t.Expires) row.ScheduleScanByIndex(2, &t.AccessTokenExpires)
row.ScheduleScanByIndex(3, &t.RefreshToken) row.ScheduleScanByIndex(3, &t.RefreshToken)
row.ScheduleScanByIndex(4, &t.Username) row.ScheduleScanByIndex(4, &t.Username)
row.ScheduleScanByIndex(5, &t.UserID) row.ScheduleScanByIndex(5, &t.UserID)
row.ScheduleScanByIndex(6, &t.ArcgisID) row.ScheduleScanByIndex(6, &t.ArcgisID)
row.ScheduleScanByIndex(7, &t.ArcgisLicenseTypeID) row.ScheduleScanByIndex(7, &t.ArcgisLicenseTypeID)
row.ScheduleScanByIndex(8, &t.RefreshTokenExpires)
return &t, nil return &t, nil
}, func(v any) (OauthTokenByUserIdRow, error) { }, func(v any) (OauthTokenByUserIdRow, error) {
return *(v.(*OauthTokenByUserIdRow)), nil return *(v.(*OauthTokenByUserIdRow)), nil
@ -57,9 +58,9 @@ func OauthTokenByUserId(UserID int32) *OauthTokenByUserIdQuery {
}, },
}, },
Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) { Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) {
q.AppendSelect(expressionTypArgs.subExpr(7, 357)) q.AppendSelect(expressionTypArgs.subExpr(7, 449))
q.SetTable(expressionTypArgs.subExpr(363, 374)) q.SetTable(expressionTypArgs.subExpr(455, 466))
q.AppendWhere(expressionTypArgs.subExpr(382, 394)) q.AppendWhere(expressionTypArgs.subExpr(474, 486))
}), }),
} }
} }
@ -67,12 +68,13 @@ func OauthTokenByUserId(UserID int32) *OauthTokenByUserIdQuery {
type OauthTokenByUserIdRow = struct { type OauthTokenByUserIdRow = struct {
ID int32 `db:"id"` ID int32 `db:"id"`
AccessToken string `db:"access_token"` AccessToken string `db:"access_token"`
Expires time.Time `db:"expires"` AccessTokenExpires time.Time `db:"access_token_expires"`
RefreshToken string `db:"refresh_token"` RefreshToken string `db:"refresh_token"`
Username string `db:"username"` Username string `db:"username"`
UserID int32 `db:"user_id"` UserID int32 `db:"user_id"`
ArcgisID null.Val[string] `db:"arcgis_id"` ArcgisID null.Val[string] `db:"arcgis_id"`
ArcgisLicenseTypeID null.Val[string] `db:"arcgis_license_type_id"` ArcgisLicenseTypeID null.Val[string] `db:"arcgis_license_type_id"`
RefreshTokenExpires time.Time `db:"refresh_token_expires"`
} }
type oauthTokenByUserIdTransformer = bob.SliceTransformer[OauthTokenByUserIdRow, []OauthTokenByUserIdRow] type oauthTokenByUserIdTransformer = bob.SliceTransformer[OauthTokenByUserIdRow, []OauthTokenByUserIdRow]
@ -85,8 +87,8 @@ func (o oauthTokenByUserId) args() iter.Seq[orm.ArgWithPosition] {
return func(yield func(arg orm.ArgWithPosition) bool) { return func(yield func(arg orm.ArgWithPosition) bool) {
if !yield(orm.ArgWithPosition{ if !yield(orm.ArgWithPosition{
Name: "userID", Name: "userID",
Start: 392, Start: 484,
Stop: 394, Stop: 486,
Expression: o.UserID, Expression: o.UserID,
}) { }) {
return return

View file

@ -2,5 +2,5 @@
-- This file is meant to be re-generated in place and/or deleted at any time. -- This file is meant to be re-generated in place and/or deleted at any time.
-- OauthTokenByUserId -- OauthTokenByUserId
SELECT "oauth_token"."id" AS "id", "oauth_token"."access_token" AS "access_token", "oauth_token"."expires" AS "expires", "oauth_token"."refresh_token" AS "refresh_token", "oauth_token"."username" AS "username", "oauth_token"."user_id" AS "user_id", "oauth_token"."arcgis_id" AS "arcgis_id", "oauth_token"."arcgis_license_type_id" AS "arcgis_license_type_id" FROM oauth_token WHERE SELECT "oauth_token"."id" AS "id", "oauth_token"."access_token" AS "access_token", "oauth_token"."access_token_expires" AS "access_token_expires", "oauth_token"."refresh_token" AS "refresh_token", "oauth_token"."username" AS "username", "oauth_token"."user_id" AS "user_id", "oauth_token"."arcgis_id" AS "arcgis_id", "oauth_token"."arcgis_license_type_id" AS "arcgis_license_type_id", "oauth_token"."refresh_token_expires" AS "refresh_token_expires" FROM oauth_token WHERE
user_id = $1; user_id = $1;

View file

@ -84,8 +84,8 @@ func TestOauthTokenByUserId(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if len(columns) != 8 { if len(columns) != 9 {
t.Fatalf("expected %d columns, got %d", 8, len(columns)) t.Fatalf("expected %d columns, got %d", 9, len(columns))
} }
if columns[0] != "id" { if columns[0] != "id" {
@ -96,8 +96,8 @@ func TestOauthTokenByUserId(t *testing.T) {
t.Fatalf("expected column %d to be %s, got %s", 1, "access_token", columns[1]) t.Fatalf("expected column %d to be %s, got %s", 1, "access_token", columns[1])
} }
if columns[2] != "expires" { if columns[2] != "access_token_expires" {
t.Fatalf("expected column %d to be %s, got %s", 2, "expires", columns[2]) t.Fatalf("expected column %d to be %s, got %s", 2, "access_token_expires", columns[2])
} }
if columns[3] != "refresh_token" { if columns[3] != "refresh_token" {
@ -119,5 +119,9 @@ func TestOauthTokenByUserId(t *testing.T) {
if columns[7] != "arcgis_license_type_id" { if columns[7] != "arcgis_license_type_id" {
t.Fatalf("expected column %d to be %s, got %s", 7, "arcgis_license_type_id", columns[7]) t.Fatalf("expected column %d to be %s, got %s", 7, "arcgis_license_type_id", columns[7])
} }
if columns[8] != "refresh_token_expires" {
t.Fatalf("expected column %d to be %s, got %s", 8, "refresh_token_expires", columns[8])
}
}) })
} }