diff --git a/arcgis.go b/arcgis.go index 9d3008c8..77674623 100644 --- a/arcgis.go +++ b/arcgis.go @@ -17,6 +17,7 @@ import ( "github.com/Gleipnir-Technology/arcgis-go" "github.com/Gleipnir-Technology/nidus-sync/models" + "github.com/Gleipnir-Technology/nidus-sync/sql" "github.com/aarondl/opt/omit" ) @@ -155,6 +156,13 @@ func handleOauthAccessCode(ctx context.Context, user *models.User, code string) return nil } +func hasFieldseekerConnection(ctx context.Context, user *models.User) (bool, error) { + result, err := sql.OauthTokenByUserId(user.ID).All(ctx, PGInstance.BobDB) + if err != nil { + return false, err + } + return len(result) > 0, nil +} func redirectURL() string { return BaseURL + "/arcgis/oauth/callback" } diff --git a/endpoint.go b/endpoint.go index 852cfb1d..012da7ae 100644 --- a/endpoint.go +++ b/endpoint.go @@ -167,7 +167,16 @@ func getRoot(w http.ResponseWriter, r *http.Request) { errorCode := r.URL.Query().Get("error") err = htmlSignin(w, errorCode) } else { - err = htmlDashboard(w, user) + has, err := hasFieldseekerConnection(r.Context(), user) + if err != nil { + respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError) + return + } + if has { + err = htmlDashboard(w, user) + } else { + err = htmlOauthPrompt(w, user) + } } if err != nil { respondError(w, "Failed to render root", err, http.StatusInternalServerError) diff --git a/html.go b/html.go index 2833fcdf..6efb9d3a 100644 --- a/html.go +++ b/html.go @@ -15,6 +15,7 @@ import ( var ( dashboard = newBuiltTemplate("dashboard", "authenticated") + oauthPrompt = newBuiltTemplate("oauth-prompt", "authenticated") report = newBuiltTemplate("report", "base") reportConfirmation = newBuiltTemplate("report-confirmation", "base") reportContribute = newBuiltTemplate("report-contribute", "base") @@ -99,6 +100,17 @@ func htmlDashboard(w io.Writer, user *models.User) error { return dashboard.ExecuteTemplate(w, data) } +func htmlOauthPrompt(w io.Writer, user *models.User) error { + data := ContentDashboard{ + User: User{ + DisplayName: user.DisplayName, + Initials: extractInitials(user.DisplayName), + Username: user.Username, + }, + } + return oauthPrompt.ExecuteTemplate(w, data) +} + func htmlReport(w io.Writer) error { url := BaseURL + "/report/t78fd3" data := ContentReportDiagnostic{ diff --git a/sql/oauth_by_user_id.bob.go b/sql/oauth_by_user_id.bob.go new file mode 100644 index 00000000..81932e6a --- /dev/null +++ b/sql/oauth_by_user_id.bob.go @@ -0,0 +1,102 @@ +// Code generated by BobGen psql v0.41.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sql + +import ( + "context" + _ "embed" + "io" + "iter" + "time" + + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/psql" + "github.com/stephenafamo/bob/dialect/psql/dialect" + "github.com/stephenafamo/bob/orm" + "github.com/stephenafamo/scan" +) + +//go:embed oauth_by_user_id.bob.sql +var formattedQueries_oauth_by_user_id string + +var oauthTokenByUserIdSQL = formattedQueries_oauth_by_user_id[156:440] + +type OauthTokenByUserIdQuery = orm.ModQuery[*dialect.SelectQuery, oauthTokenByUserId, OauthTokenByUserIdRow, []OauthTokenByUserIdRow, oauthTokenByUserIdTransformer] + +func OauthTokenByUserId(UserID int32) *OauthTokenByUserIdQuery { + var expressionTypArgs oauthTokenByUserId + + expressionTypArgs.UserID = psql.Arg(UserID) + + return &OauthTokenByUserIdQuery{ + Query: orm.Query[oauthTokenByUserId, OauthTokenByUserIdRow, []OauthTokenByUserIdRow, oauthTokenByUserIdTransformer]{ + ExecQuery: orm.ExecQuery[oauthTokenByUserId]{ + BaseQuery: bob.BaseQuery[oauthTokenByUserId]{ + Expression: expressionTypArgs, + Dialect: dialect.Dialect, + QueryType: bob.QueryTypeSelect, + }, + }, + Scanner: func(context.Context, []string) (func(*scan.Row) (any, error), func(any) (OauthTokenByUserIdRow, error)) { + return func(row *scan.Row) (any, error) { + var t OauthTokenByUserIdRow + row.ScheduleScanByIndex(0, &t.ID) + row.ScheduleScanByIndex(1, &t.AccessToken) + row.ScheduleScanByIndex(2, &t.Expires) + row.ScheduleScanByIndex(3, &t.RefreshToken) + row.ScheduleScanByIndex(4, &t.Username) + row.ScheduleScanByIndex(5, &t.UserID) + return &t, nil + }, func(v any) (OauthTokenByUserIdRow, error) { + return *(v.(*OauthTokenByUserIdRow)), nil + } + }, + }, + Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) { + q.AppendSelect(expressionTypArgs.subExpr(7, 247)) + q.SetTable(expressionTypArgs.subExpr(253, 264)) + q.AppendWhere(expressionTypArgs.subExpr(272, 284)) + }), + } +} + +type OauthTokenByUserIdRow = struct { + ID int32 `db:"id"` + AccessToken string `db:"access_token"` + Expires time.Time `db:"expires"` + RefreshToken string `db:"refresh_token"` + Username string `db:"username"` + UserID int32 `db:"user_id"` +} + +type oauthTokenByUserIdTransformer = bob.SliceTransformer[OauthTokenByUserIdRow, []OauthTokenByUserIdRow] + +type oauthTokenByUserId struct { + UserID bob.Expression +} + +func (o oauthTokenByUserId) args() iter.Seq[orm.ArgWithPosition] { + return func(yield func(arg orm.ArgWithPosition) bool) { + if !yield(orm.ArgWithPosition{ + Name: "userID", + Start: 282, + Stop: 284, + Expression: o.UserID, + }) { + return + } + } +} + +func (o oauthTokenByUserId) raw(from, to int) string { + return oauthTokenByUserIdSQL[from:to] +} + +func (o oauthTokenByUserId) subExpr(from, to int) bob.Expression { + return orm.ArgsToExpression(oauthTokenByUserIdSQL, from, to, o.args()) +} + +func (o oauthTokenByUserId) WriteSQL(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) { + return o.subExpr(0, len(oauthTokenByUserIdSQL)).WriteSQL(ctx, w, d, start) +} diff --git a/sql/oauth_by_user_id.bob.sql b/sql/oauth_by_user_id.bob.sql new file mode 100644 index 00000000..cb0f6380 --- /dev/null +++ b/sql/oauth_by_user_id.bob.sql @@ -0,0 +1,6 @@ +-- 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. + +-- 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" FROM oauth_token WHERE + user_id = $1; diff --git a/sql/oauth_by_user_id.bob_test.go b/sql/oauth_by_user_id.bob_test.go new file mode 100644 index 00000000..4a610d1a --- /dev/null +++ b/sql/oauth_by_user_id.bob_test.go @@ -0,0 +1,115 @@ +// Code generated by BobGen psql v0.41.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sql + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/psql" + testutils "github.com/stephenafamo/bob/test/utils" +) + +func TestOauthTokenByUserId(t *testing.T) { + t.Run("Base", func(t *testing.T) { + var sb strings.Builder + + query := OauthTokenByUserId(random_int32(nil)) + + if _, err := query.WriteQuery(t.Context(), &sb, 1); err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(oauthTokenByUserIdSQL, sb.String()); diff != "" { + t.Fatalf("unexpected result (-got +want):\n%s", diff) + } + }) + + t.Run("Mod", func(t *testing.T) { + var sb strings.Builder + + query := OauthTokenByUserId(random_int32(nil)) + + if _, err := psql.Select(query).WriteQuery(t.Context(), &sb, 1); err != nil { + t.Fatal(err) + } + + queryDiff, err := testutils.QueryDiff(oauthTokenByUserIdSQL, sb.String(), formatQuery) + if err != nil { + t.Fatal(err) + } + if queryDiff != "" { + fmt.Println(sb.String()) + t.Fatalf("unexpected result (-got +want):\n%s", queryDiff) + } + }) + + t.Run("Scanning", func(t *testing.T) { + if testDB == nil { + t.Skip("skipping test, no DSN provided") + } + + ctxTx, cancel := context.WithCancel(t.Context()) + defer cancel() + + tx, err := testDB.Begin(ctxTx) + if err != nil { + t.Fatalf("Error starting transaction: %v", err) + } + + defer func() { + if err := tx.Rollback(ctxTx); err != nil { + t.Fatalf("Error rolling back transaction: %v", err) + } + }() + + query, args, err := bob.Build(ctxTx, psql.Select(OauthTokenByUserId(random_int32(nil)))) + if err != nil { + t.Fatal(err) + } + + rows, err := tx.QueryContext(ctxTx, query, args...) + if err != nil { + t.Fatal(err) + } + defer rows.Close() + + columns, err := rows.Columns() + if err != nil { + t.Fatal(err) + } + + if len(columns) != 6 { + t.Fatalf("expected %d columns, got %d", 6, len(columns)) + } + + if columns[0] != "id" { + t.Fatalf("expected column %d to be %s, got %s", 0, "id", columns[0]) + } + + if columns[1] != "access_token" { + t.Fatalf("expected column %d to be %s, got %s", 1, "access_token", columns[1]) + } + + if columns[2] != "expires" { + t.Fatalf("expected column %d to be %s, got %s", 2, "expires", columns[2]) + } + + if columns[3] != "refresh_token" { + t.Fatalf("expected column %d to be %s, got %s", 3, "refresh_token", columns[3]) + } + + if columns[4] != "username" { + t.Fatalf("expected column %d to be %s, got %s", 4, "username", columns[4]) + } + + if columns[5] != "user_id" { + t.Fatalf("expected column %d to be %s, got %s", 5, "user_id", columns[5]) + } + }) +} diff --git a/sql/oauth_by_user_id.sql b/sql/oauth_by_user_id.sql new file mode 100644 index 00000000..40a297b2 --- /dev/null +++ b/sql/oauth_by_user_id.sql @@ -0,0 +1,3 @@ +-- OauthTokenByUserId +SELECT * FROM oauth_token WHERE + user_id = $1; diff --git a/templates/authenticated.html b/templates/authenticated.html index d898960f..1a3509dc 100644 --- a/templates/authenticated.html +++ b/templates/authenticated.html @@ -5,6 +5,7 @@
Link your data to get started
-To provide you with the best experience, we need to connect to your ArcGIS account. This allows us to securely access and visualize your spatial data within our platform.
- -When you click the "Connect to ArcGIS" button below, you'll be redirected to the official ArcGIS login page. This connection is secure and uses OAuth 2.0 protocol.
-After logging in with your ArcGIS credentials, you'll be asked to approve permissions for our application to access your data. We only request access to what's needed for the platform to function.
-Once authentication is complete, you'll be automatically redirected back to our platform where your data will be available to work with.
-By connecting your ArcGIS account, you'll be able to:
-You can disconnect your account at any time in settings
-Overview of mosquito control activities in your district
++ Last updated: 3 hours ago + +
+3h
+Last sync: 12:45 PM
+48
++ + 12% + since last week +
+124
++ + 8% + since last month +
+76
++ + 15% + since last week +
+Map visualization will be displayed here
+ Showing activity data for the Central District area +Link your data to get started
+To provide you with the best experience, we need to connect to your ArcGIS account. This allows us to securely access and visualize your spatial data within our platform.
+ +When you click the "Connect to ArcGIS" button below, you'll be redirected to the official ArcGIS login page. This connection is secure and uses OAuth 2.0 protocol.
+After logging in with your ArcGIS credentials, you'll be asked to approve permissions for our application to access your data. We only request access to what's needed for the platform to function.
+Once authentication is complete, you'll be automatically redirected back to our platform where your data will be available to work with.
+By connecting your ArcGIS account, you'll be able to:
+You can disconnect your account at any time in settings
+