Show actual information on oauth integration setting page

This commit is contained in:
Eli Ribble 2026-02-13 21:14:46 +00:00
parent e475e43fef
commit bdd862e649
No known key found for this signature in database
14 changed files with 156 additions and 21 deletions

15
arcgis/oauth.go Normal file
View file

@ -0,0 +1,15 @@
package arcgis
import (
"context"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
)
func GetOAuthForUser(ctx context.Context, user *models.User) (*models.OauthToken, error) {
return user.UserOauthTokens(
sm.OrderBy("created").Desc(),
).One(ctx, db.PGInstance.BobDB)
}

View file

@ -110,6 +110,7 @@ func HandleOauthAccessCode(ctx context.Context, user *models.User, code string)
AccessTokenExpires: omit.From(accessExpires), AccessTokenExpires: omit.From(accessExpires),
ArcgisID: omitnull.FromPtr[string](nil), ArcgisID: omitnull.FromPtr[string](nil),
ArcgisLicenseTypeID: omitnull.FromPtr[string](nil), ArcgisLicenseTypeID: omitnull.FromPtr[string](nil),
Created: omit.From(time.Now()),
InvalidatedAt: omitnull.FromPtr[time.Time](nil), InvalidatedAt: omitnull.FromPtr[time.Time](nil),
RefreshToken: omit.From(token.RefreshToken), RefreshToken: omit.From(token.RefreshToken),
RefreshTokenExpires: omit.From(refreshExpires), RefreshTokenExpires: omit.From(refreshExpires),

View file

@ -105,6 +105,15 @@ var OauthTokens = Table[
Generated: false, Generated: false,
AutoIncr: false, AutoIncr: false,
}, },
Created: column{
Name: "created",
DBType: "timestamp without time zone",
Default: "",
Comment: "",
Nullable: false,
Generated: false,
AutoIncr: false,
},
}, },
Indexes: oauthTokenIndexes{ Indexes: oauthTokenIndexes{
OauthTokenPkey: index{ OauthTokenPkey: index{
@ -156,11 +165,12 @@ type oauthTokenColumns struct {
ArcgisLicenseTypeID column ArcgisLicenseTypeID column
RefreshTokenExpires column RefreshTokenExpires column
InvalidatedAt column InvalidatedAt column
Created column
} }
func (c oauthTokenColumns) AsSlice() []column { func (c oauthTokenColumns) AsSlice() []column {
return []column{ return []column{
c.ID, c.AccessToken, c.AccessTokenExpires, c.RefreshToken, c.Username, c.UserID, c.ArcgisID, c.ArcgisLicenseTypeID, c.RefreshTokenExpires, c.InvalidatedAt, c.ID, c.AccessToken, c.AccessTokenExpires, c.RefreshToken, c.Username, c.UserID, c.ArcgisID, c.ArcgisLicenseTypeID, c.RefreshTokenExpires, c.InvalidatedAt, c.Created,
} }
} }

View file

@ -2912,6 +2912,7 @@ func (f *Factory) FromExistingOauthToken(m *models.OauthToken) *OauthTokenTempla
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 } o.RefreshTokenExpires = func() time.Time { return m.RefreshTokenExpires }
o.InvalidatedAt = func() null.Val[time.Time] { return m.InvalidatedAt } o.InvalidatedAt = func() null.Val[time.Time] { return m.InvalidatedAt }
o.Created = func() time.Time { return m.Created }
ctx := context.Background() ctx := context.Background()
if m.R.UserUser != nil { if m.R.UserUser != nil {

View file

@ -47,6 +47,7 @@ type OauthTokenTemplate struct {
ArcgisLicenseTypeID func() null.Val[string] ArcgisLicenseTypeID func() null.Val[string]
RefreshTokenExpires func() time.Time RefreshTokenExpires func() time.Time
InvalidatedAt func() null.Val[time.Time] InvalidatedAt func() null.Val[time.Time]
Created func() time.Time
r oauthTokenR r oauthTokenR
f *Factory f *Factory
@ -125,6 +126,10 @@ func (o OauthTokenTemplate) BuildSetter() *models.OauthTokenSetter {
val := o.InvalidatedAt() val := o.InvalidatedAt()
m.InvalidatedAt = omitnull.FromNull(val) m.InvalidatedAt = omitnull.FromNull(val)
} }
if o.Created != nil {
val := o.Created()
m.Created = omit.From(val)
}
return m return m
} }
@ -177,6 +182,9 @@ func (o OauthTokenTemplate) Build() *models.OauthToken {
if o.InvalidatedAt != nil { if o.InvalidatedAt != nil {
m.InvalidatedAt = o.InvalidatedAt() m.InvalidatedAt = o.InvalidatedAt()
} }
if o.Created != nil {
m.Created = o.Created()
}
o.setModelRels(m) o.setModelRels(m)
@ -217,6 +225,10 @@ func ensureCreatableOauthToken(m *models.OauthTokenSetter) {
val := random_int32(nil) val := random_int32(nil)
m.UserID = omit.From(val) m.UserID = omit.From(val)
} }
if !(m.Created.IsValue()) {
val := random_time_Time(nil)
m.Created = omit.From(val)
}
} }
// insertOptRels creates and inserts any optional the relationships on *models.OauthToken // insertOptRels creates and inserts any optional the relationships on *models.OauthToken
@ -346,6 +358,7 @@ func (m oauthTokenMods) RandomizeAllColumns(f *faker.Faker) OauthTokenMod {
OauthTokenMods.RandomArcgisLicenseTypeID(f), OauthTokenMods.RandomArcgisLicenseTypeID(f),
OauthTokenMods.RandomRefreshTokenExpires(f), OauthTokenMods.RandomRefreshTokenExpires(f),
OauthTokenMods.RandomInvalidatedAt(f), OauthTokenMods.RandomInvalidatedAt(f),
OauthTokenMods.RandomCreated(f),
} }
} }
@ -725,6 +738,37 @@ func (m oauthTokenMods) RandomInvalidatedAtNotNull(f *faker.Faker) OauthTokenMod
}) })
} }
// Set the model columns to this value
func (m oauthTokenMods) Created(val time.Time) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Created = func() time.Time { return val }
})
}
// Set the Column from the function
func (m oauthTokenMods) CreatedFunc(f func() time.Time) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Created = f
})
}
// Clear any values for the column
func (m oauthTokenMods) UnsetCreated() OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Created = nil
})
}
// Generates a random value for the column using the given faker
// if faker is nil, a default faker is used
func (m oauthTokenMods) RandomCreated(f *faker.Faker) OauthTokenMod {
return OauthTokenModFunc(func(_ context.Context, o *OauthTokenTemplate) {
o.Created = 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

@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE oauth_token ADD COLUMN created TIMESTAMP WITHOUT TIME ZONE;
UPDATE oauth_token SET created = now();
ALTER TABLE oauth_token ALTER COLUMN created SET NOT NULL;
-- +goose Down
ALTER TABLE oauth_token DROP COLUMN created;

View file

@ -36,6 +36,7 @@ type OauthToken struct {
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" ` RefreshTokenExpires time.Time `db:"refresh_token_expires" `
InvalidatedAt null.Val[time.Time] `db:"invalidated_at" ` InvalidatedAt null.Val[time.Time] `db:"invalidated_at" `
Created time.Time `db:"created" `
R oauthTokenR `db:"-" ` R oauthTokenR `db:"-" `
} }
@ -58,7 +59,7 @@ 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", "access_token_expires", "refresh_token", "username", "user_id", "arcgis_id", "arcgis_license_type_id", "refresh_token_expires", "invalidated_at", "id", "access_token", "access_token_expires", "refresh_token", "username", "user_id", "arcgis_id", "arcgis_license_type_id", "refresh_token_expires", "invalidated_at", "created",
).WithParent("oauth_token"), ).WithParent("oauth_token"),
tableAlias: alias, tableAlias: alias,
ID: psql.Quote(alias, "id"), ID: psql.Quote(alias, "id"),
@ -71,6 +72,7 @@ func buildOauthTokenColumns(alias string) oauthTokenColumns {
ArcgisLicenseTypeID: psql.Quote(alias, "arcgis_license_type_id"), ArcgisLicenseTypeID: psql.Quote(alias, "arcgis_license_type_id"),
RefreshTokenExpires: psql.Quote(alias, "refresh_token_expires"), RefreshTokenExpires: psql.Quote(alias, "refresh_token_expires"),
InvalidatedAt: psql.Quote(alias, "invalidated_at"), InvalidatedAt: psql.Quote(alias, "invalidated_at"),
Created: psql.Quote(alias, "created"),
} }
} }
@ -87,6 +89,7 @@ type oauthTokenColumns struct {
ArcgisLicenseTypeID psql.Expression ArcgisLicenseTypeID psql.Expression
RefreshTokenExpires psql.Expression RefreshTokenExpires psql.Expression
InvalidatedAt psql.Expression InvalidatedAt psql.Expression
Created psql.Expression
} }
func (c oauthTokenColumns) Alias() string { func (c oauthTokenColumns) Alias() string {
@ -111,10 +114,11 @@ type OauthTokenSetter struct {
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" ` RefreshTokenExpires omit.Val[time.Time] `db:"refresh_token_expires" `
InvalidatedAt omitnull.Val[time.Time] `db:"invalidated_at" ` InvalidatedAt omitnull.Val[time.Time] `db:"invalidated_at" `
Created omit.Val[time.Time] `db:"created" `
} }
func (s OauthTokenSetter) SetColumns() []string { func (s OauthTokenSetter) SetColumns() []string {
vals := make([]string, 0, 10) vals := make([]string, 0, 11)
if s.ID.IsValue() { if s.ID.IsValue() {
vals = append(vals, "id") vals = append(vals, "id")
} }
@ -145,6 +149,9 @@ func (s OauthTokenSetter) SetColumns() []string {
if !s.InvalidatedAt.IsUnset() { if !s.InvalidatedAt.IsUnset() {
vals = append(vals, "invalidated_at") vals = append(vals, "invalidated_at")
} }
if s.Created.IsValue() {
vals = append(vals, "created")
}
return vals return vals
} }
@ -179,6 +186,9 @@ func (s OauthTokenSetter) Overwrite(t *OauthToken) {
if !s.InvalidatedAt.IsUnset() { if !s.InvalidatedAt.IsUnset() {
t.InvalidatedAt = s.InvalidatedAt.MustGetNull() t.InvalidatedAt = s.InvalidatedAt.MustGetNull()
} }
if s.Created.IsValue() {
t.Created = s.Created.MustGet()
}
} }
func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) { func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
@ -187,7 +197,7 @@ func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
}) })
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) {
vals := make([]bob.Expression, 10) vals := make([]bob.Expression, 11)
if s.ID.IsValue() { if s.ID.IsValue() {
vals[0] = psql.Arg(s.ID.MustGet()) vals[0] = psql.Arg(s.ID.MustGet())
} else { } else {
@ -248,6 +258,12 @@ func (s *OauthTokenSetter) Apply(q *dialect.InsertQuery) {
vals[9] = psql.Raw("DEFAULT") vals[9] = psql.Raw("DEFAULT")
} }
if s.Created.IsValue() {
vals[10] = psql.Arg(s.Created.MustGet())
} else {
vals[10] = psql.Raw("DEFAULT")
}
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
})) }))
} }
@ -257,7 +273,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, 10) exprs := make([]bob.Expression, 0, 11)
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{
@ -329,6 +345,13 @@ func (s OauthTokenSetter) Expressions(prefix ...string) []bob.Expression {
}}) }})
} }
if s.Created.IsValue() {
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
psql.Quote(append(prefix, "created")...),
psql.Arg(s.Created),
}})
}
return exprs return exprs
} }
@ -638,6 +661,7 @@ type oauthTokenWhere[Q psql.Filterable] struct {
ArcgisLicenseTypeID psql.WhereNullMod[Q, string] ArcgisLicenseTypeID psql.WhereNullMod[Q, string]
RefreshTokenExpires psql.WhereMod[Q, time.Time] RefreshTokenExpires psql.WhereMod[Q, time.Time]
InvalidatedAt psql.WhereNullMod[Q, time.Time] InvalidatedAt psql.WhereNullMod[Q, time.Time]
Created psql.WhereMod[Q, time.Time]
} }
func (oauthTokenWhere[Q]) AliasedAs(alias string) oauthTokenWhere[Q] { func (oauthTokenWhere[Q]) AliasedAs(alias string) oauthTokenWhere[Q] {
@ -656,6 +680,7 @@ func buildOauthTokenWhere[Q psql.Filterable](cols oauthTokenColumns) oauthTokenW
ArcgisLicenseTypeID: psql.WhereNull[Q, string](cols.ArcgisLicenseTypeID), ArcgisLicenseTypeID: psql.WhereNull[Q, string](cols.ArcgisLicenseTypeID),
RefreshTokenExpires: psql.Where[Q, time.Time](cols.RefreshTokenExpires), RefreshTokenExpires: psql.Where[Q, time.Time](cols.RefreshTokenExpires),
InvalidatedAt: psql.WhereNull[Q, time.Time](cols.InvalidatedAt), InvalidatedAt: psql.WhereNull[Q, time.Time](cols.InvalidatedAt),
Created: psql.Where[Q, time.Time](cols.Created),
} }
} }

View file

@ -1,6 +1,6 @@
{{ template "sync/layout/base.html" . }} {{ template "sync/layout/authenticated.html" . }}
{{ define "title" }}Dash{{ end }} {{ define "title" }}Settings - Integrations{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
<style> <style>
.integration-card { .integration-card {
@ -69,30 +69,36 @@
<tr> <tr>
<td width="30%"><strong>OAuth Token Status</strong></td> <td width="30%"><strong>OAuth Token Status</strong></td>
<td> <td>
<span class="status-active"> {{ if .ArcGISOAuth.InvalidatedAt.IsNull }}
<i class="bi bi-check-circle-fill me-1"></i> Active <span class="status-active">
</span> <i class="bi bi-check-circle-fill me-1"></i> Active
</span>
{{ else }}
<span class="status-inactive">
<i class="bi bi-x-circle-fill me-1"></i> Active
</span>
{{ end }}
</td> </td>
</tr> </tr>
<tr> <tr>
<td><strong>Token Expiration</strong></td> <td><strong>Token Expiration</strong></td>
<td>26 days remaining (Expires on Dec 31, 2025)</td> <td>{{ .ArcGISOAuth.AccessTokenExpires|timeSince }}</td>
</tr> </tr>
<tr> <tr>
<td><strong>Integration Method</strong></td> <td><strong>Integration Method</strong></td>
<td>Web Hooks</td> <td>Polling</td>
</tr> </tr>
<tr> <tr>
<td><strong>Permission Level</strong></td> <td><strong>Permission Level</strong></td>
<td>Read & Write</td> <td>Read</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button class="btn btn-primary"> <a class="btn btn-primary" href="{{ .URL.OAuthRefreshArcGIS }}">
<i class="bi bi-arrow-repeat me-2"></i>Refresh OAuth Token <i class="bi bi-arrow-repeat me-2"></i>Refresh OAuth Token
</button> </a>
<button class="btn btn-outline-danger"> <button class="btn btn-outline-danger">
<i class="bi bi-trash me-2"></i>Delete Token <i class="bi bi-trash me-2"></i>Delete Token
</button> </button>

View file

@ -1,6 +1,6 @@
{{ template "sync/layout/authenticated.html" . }} {{ template "sync/layout/authenticated.html" . }}
{{ define "title" }}Dash{{ end }} {{ define "title" }}Settings{{ end }}
{{ define "extraheader" }} {{ define "extraheader" }}
<style> <style>
.settings-card { .settings-card {

View file

@ -36,7 +36,6 @@ import (
serviceRequestQuickConfirmation = buildTemplate("service-request-quick-confirmation", "base") serviceRequestQuickConfirmation = buildTemplate("service-request-quick-confirmation", "base")
serviceRequestUpdates = buildTemplate("service-request-updates", "base") serviceRequestUpdates = buildTemplate("service-request-updates", "base")
settingRoot = buildTemplate("setting-mock", "base") settingRoot = buildTemplate("setting-mock", "base")
settingIntegration = buildTemplate("setting-integration", "base")
settingPesticide = buildTemplate("setting-pesticide", "base") settingPesticide = buildTemplate("setting-pesticide", "base")
settingPesticideAdd = buildTemplate("setting-pesticide-add", "base") settingPesticideAdd = buildTemplate("setting-pesticide-add", "base")
settingUsers = buildTemplate("setting-user", "base") settingUsers = buildTemplate("setting-user", "base")

View file

@ -66,7 +66,8 @@ func Router() chi.Router {
r.Method("GET", "/pool/upload", auth.NewEnsureAuth(getPoolUpload)) r.Method("GET", "/pool/upload", auth.NewEnsureAuth(getPoolUpload))
r.Method("GET", "/pool/upload/{id}", auth.NewEnsureAuth(getPoolUploadByID)) r.Method("GET", "/pool/upload/{id}", auth.NewEnsureAuth(getPoolUploadByID))
r.Method("POST", "/pool/upload", auth.NewEnsureAuth(postPoolUpload)) r.Method("POST", "/pool/upload", auth.NewEnsureAuth(postPoolUpload))
r.Method("GET", "/setting", auth.NewEnsureAuth(getSettings)) r.Method("GET", "/setting", auth.NewEnsureAuth(getSetting))
r.Method("GET", "/setting/integration", auth.NewEnsureAuth(getSettingIntegration))
r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout))
r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource))
r.Method("GET", "/stadia", auth.NewEnsureAuth(getStadia)) r.Method("GET", "/stadia", auth.NewEnsureAuth(getStadia))

View file

@ -3,11 +3,19 @@ package sync
import ( import (
"net/http" "net/http"
"github.com/Gleipnir-Technology/nidus-sync/arcgis"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html" "github.com/Gleipnir-Technology/nidus-sync/html"
) )
func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) { type ContentSettingIntegration struct {
ArcGISOAuth *models.OauthToken
URL ContentURL
User User
}
func getSetting(w http.ResponseWriter, r *http.Request, u *models.User) {
userContent, err := contentForUser(r.Context(), u) userContent, err := contentForUser(r.Context(), u)
if err != nil { if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError) respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
@ -19,3 +27,22 @@ func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) {
} }
html.RenderOrError(w, "sync/settings.html", data) html.RenderOrError(w, "sync/settings.html", data)
} }
func getSettingIntegration(w http.ResponseWriter, r *http.Request, u *models.User) {
ctx := r.Context()
userContent, err := contentForUser(ctx, u)
if err != nil {
respondError(w, "Failed to get user content", err, http.StatusInternalServerError)
return
}
oauth, err := arcgis.GetOAuthForUser(ctx, u)
if err != nil {
respondError(w, "Failed to get oauth", err, http.StatusInternalServerError)
return
}
data := ContentSettingIntegration{
ArcGISOAuth: oauth,
URL: newContentURL(),
User: userContent,
}
html.RenderOrError(w, "sync/setting-integration.html", data)
}

View file

@ -60,8 +60,6 @@ type ContentDashboardLoading struct {
User User User User
} }
type ContentPlaceholder struct {
}
type ContentSignin struct { type ContentSignin struct {
InvalidCredentials bool InvalidCredentials bool
} }

View file

@ -5,6 +5,7 @@ import (
) )
type ContentURL struct { type ContentURL struct {
OAuthRefreshArcGIS string
PoolCSVUpload string PoolCSVUpload string
SamplePoolCSV string SamplePoolCSV string
Setting string Setting string
@ -18,6 +19,7 @@ type ContentURL struct {
func newContentURL() ContentURL { func newContentURL() ContentURL {
return ContentURL{ return ContentURL{
OAuthRefreshArcGIS: config.MakeURLNidus("/arcgis/oauth/begin"),
PoolCSVUpload: config.MakeURLNidus("/pool/upload"), PoolCSVUpload: config.MakeURLNidus("/pool/upload"),
SamplePoolCSV: config.MakeURLNidus("/static/file/sample-pool.csv"), SamplePoolCSV: config.MakeURLNidus("/static/file/sample-pool.csv"),
Setting: config.MakeURLNidus("/setting"), Setting: config.MakeURLNidus("/setting"),