2025-11-03 12:38:47 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-13 00:26:11 +00:00
|
|
|
"encoding/json"
|
2025-11-05 17:15:33 +00:00
|
|
|
"errors"
|
2025-12-12 21:53:09 +00:00
|
|
|
"fmt"
|
2025-12-12 22:17:40 +00:00
|
|
|
"io"
|
2025-11-03 12:38:47 +00:00
|
|
|
"net/http"
|
2025-11-05 21:21:58 +00:00
|
|
|
"strconv"
|
2025-11-05 17:15:33 +00:00
|
|
|
"strings"
|
2025-11-05 21:21:58 +00:00
|
|
|
|
2025-12-16 16:37:53 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/auth"
|
2025-11-24 18:08:24 +00:00
|
|
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
2025-11-05 21:21:58 +00:00
|
|
|
"github.com/go-chi/chi/v5"
|
2025-12-24 17:49:39 -07:00
|
|
|
"github.com/google/uuid"
|
2025-11-14 23:09:27 +00:00
|
|
|
"github.com/rs/zerolog/log"
|
2025-11-05 21:21:58 +00:00
|
|
|
"github.com/skip2/go-qrcode"
|
2025-11-03 12:38:47 +00:00
|
|
|
)
|
2025-11-06 00:23:58 +00:00
|
|
|
|
|
|
|
|
func getArcgisOauthBegin(w http.ResponseWriter, r *http.Request) {
|
2025-11-13 15:15:35 +00:00
|
|
|
authURL := buildArcGISAuthURL(ClientID)
|
2025-11-06 00:23:58 +00:00
|
|
|
http.Redirect(w, r, authURL, http.StatusFound)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getArcgisOauthCallback(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
code := r.URL.Query().Get("code")
|
2025-11-14 23:09:27 +00:00
|
|
|
log.Info().Str("code", code).Msg("Handling oauth callback")
|
2025-11-06 00:23:58 +00:00
|
|
|
if code == "" {
|
|
|
|
|
respondError(w, "Access code is empty", nil, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-16 16:37:53 +00:00
|
|
|
user, err := auth.GetAuthenticatedUser(r)
|
2025-11-06 00:23:58 +00:00
|
|
|
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)
|
|
|
|
|
}
|
2025-11-19 15:21:06 +00:00
|
|
|
|
|
|
|
|
func getCellDetails(w http.ResponseWriter, r *http.Request, user *models.User) {
|
|
|
|
|
cell_str := chi.URLParam(r, "cell")
|
|
|
|
|
if cell_str == "" {
|
|
|
|
|
respondError(w, "There should always be a cell", nil, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cell, err := HexToInt64(cell_str)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Cannot convert provided cell to uint64", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
htmlCell(r.Context(), w, user, cell)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-03 12:38:47 +00:00
|
|
|
func getFavicon(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-type", "image/x-icon")
|
|
|
|
|
|
|
|
|
|
http.ServeFile(w, r, "static/favicon.ico")
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 21:27:51 +00:00
|
|
|
func getOAuthRefresh(w http.ResponseWriter, r *http.Request) {
|
2025-12-16 16:37:53 +00:00
|
|
|
user, err := auth.GetAuthenticatedUser(r)
|
2025-11-12 21:27:51 +00:00
|
|
|
if err != nil {
|
|
|
|
|
http.Redirect(w, r, "/?next=/oauth/refresh", http.StatusFound)
|
2025-11-24 18:09:06 +00:00
|
|
|
return
|
2025-11-12 21:27:51 +00:00
|
|
|
}
|
|
|
|
|
htmlOauthPrompt(w, user)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 21:21:58 +00:00
|
|
|
func getQRCodeReport(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
code := chi.URLParam(r, "code")
|
|
|
|
|
if code == "" {
|
|
|
|
|
respondError(w, "There should always be a code", nil, http.StatusBadRequest)
|
|
|
|
|
}
|
|
|
|
|
content := BaseURL + "/report/" + code
|
|
|
|
|
// Get optional size parameter (default to 256)
|
|
|
|
|
size := 256
|
|
|
|
|
if sizeStr := r.URL.Query().Get("size"); sizeStr != "" {
|
|
|
|
|
var err error
|
|
|
|
|
size, err = strconv.Atoi(sizeStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Error(w, "Invalid 'size' parameter, must be an integer", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get optional error correction level (default to Medium)
|
|
|
|
|
level := qrcode.Medium
|
|
|
|
|
if levelStr := r.URL.Query().Get("level"); levelStr != "" {
|
|
|
|
|
switch levelStr {
|
|
|
|
|
case "L", "l":
|
|
|
|
|
level = qrcode.Low
|
|
|
|
|
case "M", "m":
|
|
|
|
|
level = qrcode.Medium
|
|
|
|
|
case "Q", "q":
|
|
|
|
|
level = qrcode.High
|
|
|
|
|
case "H", "h":
|
|
|
|
|
level = qrcode.Highest
|
|
|
|
|
default:
|
|
|
|
|
respondError(w, "Invalid 'level' parameter, must be L, M, Q, or H", nil, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate the QR code
|
|
|
|
|
var qr *qrcode.QRCode
|
|
|
|
|
var err error
|
|
|
|
|
qr, err = qrcode.New(content, level)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Error generating QR code", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set the appropriate content type
|
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-05 21:21:58 +00:00
|
|
|
// Generate PNG and write directly to the response writer
|
|
|
|
|
png, err := qr.PNG(size)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Error encoding QR code to PNG", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = w.Write(png)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Error writing response", err, http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-05 23:41:21 +00:00
|
|
|
|
2025-11-03 12:38:47 +00:00
|
|
|
func getRoot(w http.ResponseWriter, r *http.Request) {
|
2025-12-16 16:37:53 +00:00
|
|
|
user, err := auth.GetAuthenticatedUser(r)
|
2026-01-06 15:06:16 +00:00
|
|
|
if err != nil {
|
|
|
|
|
// No credentials or user not found: go to login
|
|
|
|
|
if errors.Is(err, &auth.NoCredentialsError{}) || errors.Is(err, &auth.NoUserError{}) {
|
|
|
|
|
http.Redirect(w, r, "/signin", http.StatusFound)
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
respondError(w, "Failed to get root", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|
|
|
|
|
if user == nil {
|
|
|
|
|
errorCode := r.URL.Query().Get("error")
|
2025-11-10 22:23:46 +00:00
|
|
|
htmlSignin(w, errorCode)
|
|
|
|
|
return
|
2025-11-05 17:15:33 +00:00
|
|
|
} else {
|
2025-11-06 22:58:18 +00:00
|
|
|
has, err := hasFieldseekerConnection(r.Context(), user)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "Failed to check for ArcGIS connection", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if has {
|
2025-11-10 22:16:23 +00:00
|
|
|
htmlDashboard(r.Context(), w, user)
|
|
|
|
|
return
|
2025-11-06 22:58:18 +00:00
|
|
|
} else {
|
2025-11-10 22:23:46 +00:00
|
|
|
htmlOauthPrompt(w, user)
|
|
|
|
|
return
|
2025-11-06 22:58:18 +00:00
|
|
|
}
|
2025-11-05 17:15:33 +00:00
|
|
|
}
|
2025-11-03 12:38:47 +00:00
|
|
|
if err != nil {
|
2025-11-05 17:15:33 +00:00
|
|
|
respondError(w, "Failed to render root", err, http.StatusInternalServerError)
|
2025-11-03 12:38:47 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-11-08 00:04:44 +00:00
|
|
|
|
2025-11-13 15:50:10 +00:00
|
|
|
func getSettings(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
htmlSettings(w, r, u)
|
|
|
|
|
}
|
2025-11-19 15:19:42 +00:00
|
|
|
|
|
|
|
|
func getSignin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
errorCode := r.URL.Query().Get("error")
|
|
|
|
|
htmlSignin(w, errorCode)
|
|
|
|
|
}
|
2025-11-20 14:56:34 +00:00
|
|
|
|
2025-11-04 00:02:51 +00:00
|
|
|
func getSignup(w http.ResponseWriter, r *http.Request) {
|
2025-11-10 22:23:46 +00:00
|
|
|
htmlSignup(w, r.URL.Path)
|
2025-11-04 00:02:51 +00:00
|
|
|
}
|
2025-11-20 14:56:34 +00:00
|
|
|
|
|
|
|
|
func getSource(w http.ResponseWriter, r *http.Request, u *models.User) {
|
2025-12-24 17:49:39 -07:00
|
|
|
globalid_s := chi.URLParam(r, "globalid")
|
|
|
|
|
if globalid_s == "" {
|
2025-11-20 14:56:34 +00:00
|
|
|
respondError(w, "No globalid provided", nil, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-24 17:49:39 -07:00
|
|
|
globalid, err := uuid.Parse(globalid_s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
respondError(w, "globalid is not a UUID", nil, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-11-20 14:56:34 +00:00
|
|
|
htmlSource(w, r, u, globalid)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 22:17:40 +00:00
|
|
|
func postSMS(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
// Log all request headers
|
|
|
|
|
for name, values := range r.Header {
|
|
|
|
|
for _, value := range values {
|
|
|
|
|
log.Info().Str("name", name).Str("value", value).Msg("header")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 00:26:11 +00:00
|
|
|
// Read the request body
|
2025-12-12 22:17:40 +00:00
|
|
|
bodyBytes, err := io.ReadAll(r.Body)
|
|
|
|
|
if err != nil {
|
2025-12-13 00:26:11 +00:00
|
|
|
//return nil, fmt.Errorf("failed to read request body: %w", err)
|
|
|
|
|
respondError(w, "Failed to read request body", err, http.StatusInternalServerError)
|
2025-12-12 22:17:40 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-13 00:26:11 +00:00
|
|
|
log.Info().Str("body", string(bodyBytes)).Msg("body")
|
2025-12-12 22:17:40 +00:00
|
|
|
// Close the original body
|
2025-12-13 00:26:11 +00:00
|
|
|
defer r.Body.Close()
|
2025-12-12 22:17:40 +00:00
|
|
|
|
2025-12-13 00:26:11 +00:00
|
|
|
// Parse JSON into webhook struct
|
|
|
|
|
var body SMSWebhookBody
|
|
|
|
|
if err := json.Unmarshal(bodyBytes, &body); err != nil {
|
|
|
|
|
respondError(w, "Failed to parse JSON", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-12 22:17:40 +00:00
|
|
|
|
2025-12-13 00:26:11 +00:00
|
|
|
if err := handleSMSMessage(&body.Data); err != nil {
|
|
|
|
|
log.Error().Err(err).Msg("Failed to handle SMS Message")
|
|
|
|
|
}
|
2025-12-12 22:17:40 +00:00
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
w.Write([]byte("ok"))
|
|
|
|
|
}
|
2025-12-12 21:53:09 +00:00
|
|
|
func getSMS(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
org := chi.URLParam(r, "org")
|
|
|
|
|
|
|
|
|
|
to := r.URL.Query().Get("error")
|
|
|
|
|
from := r.URL.Query().Get("error")
|
|
|
|
|
message := r.URL.Query().Get("error")
|
|
|
|
|
files := r.URL.Query().Get("error")
|
|
|
|
|
id := r.URL.Query().Get("error")
|
|
|
|
|
date := r.URL.Query().Get("error")
|
|
|
|
|
|
|
|
|
|
log.Info().Str("org", org).Str("to", to).Str("from", from).Str("message", message).Str("files", files).Str("id", id).Str("date", date).Msg("Got SMS Message")
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
w.Header().Set("Content-type", "text/plain")
|
|
|
|
|
// Signifies to Voip.ms that the callback worked.
|
|
|
|
|
fmt.Fprintf(w, "ok")
|
|
|
|
|
}
|
2025-11-14 23:09:27 +00:00
|
|
|
func getVectorTiles(w http.ResponseWriter, r *http.Request, u *models.User) {
|
|
|
|
|
org_id := chi.URLParam(r, "org_id")
|
|
|
|
|
tileset_id := chi.URLParam(r, "tileset_id")
|
|
|
|
|
zoom := chi.URLParam(r, "zoom")
|
|
|
|
|
x := chi.URLParam(r, "x")
|
|
|
|
|
y := chi.URLParam(r, "y")
|
|
|
|
|
format := chi.URLParam(r, "format")
|
2025-11-04 23:21:13 +00:00
|
|
|
|
2025-11-14 23:09:27 +00:00
|
|
|
log.Info().Str("org_id", org_id).Str("tileset_id", tileset_id).Str("zoom", zoom).Str("x", x).Str("y", y).Str("format", format).Msg("Get vector tiles")
|
|
|
|
|
|
|
|
|
|
}
|
2026-01-06 14:46:31 +00:00
|
|
|
|
|
|
|
|
// Respond with an error that is visible to the user
|
2025-11-05 14:15:06 +00:00
|
|
|
func respondError(w http.ResponseWriter, m string, e error, s int) {
|
2026-01-06 14:46:31 +00:00
|
|
|
log.Warn().Int("status", s).Err(e).Str("user message", m).Msg("Responding with an error")
|
|
|
|
|
http.Error(w, m, s)
|
2025-11-05 14:15:06 +00:00
|
|
|
}
|
|
|
|
|
|
2025-11-05 17:15:33 +00:00
|
|
|
func postSignin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
|
|
|
respondError(w, "Could not parse form", err, http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-05 17:15:33 +00:00
|
|
|
username := r.FormValue("username")
|
|
|
|
|
password := r.FormValue("password")
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-14 23:09:27 +00:00
|
|
|
log.Info().Str("username", username).Msg("Signin")
|
2025-11-05 17:15:33 +00:00
|
|
|
|
2025-12-16 16:37:53 +00:00
|
|
|
_, err := auth.SigninUser(r, username, password)
|
2025-11-05 17:15:33 +00:00
|
|
|
if err != nil {
|
2025-12-16 16:37:53 +00:00
|
|
|
if errors.Is(err, auth.InvalidCredentials{}) {
|
2026-01-07 03:42:18 +00:00
|
|
|
http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, auth.InvalidUsername{}) {
|
|
|
|
|
http.Redirect(w, r, "/signin?error=invalid-credentials", http.StatusFound)
|
2025-11-05 17:15:33 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
respondError(w, "Failed to signin user", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-04 23:21:13 +00:00
|
|
|
func postSignup(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
if err := r.ParseForm(); err != nil {
|
2025-11-05 14:15:06 +00:00
|
|
|
respondError(w, "Could not parse form", err, http.StatusBadRequest)
|
2025-11-04 23:21:13 +00:00
|
|
|
return
|
|
|
|
|
}
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-05 14:15:06 +00:00
|
|
|
username := r.FormValue("username")
|
2025-11-04 23:21:13 +00:00
|
|
|
name := r.FormValue("name")
|
2025-11-05 14:15:06 +00:00
|
|
|
password := r.FormValue("password")
|
2025-11-04 23:21:13 +00:00
|
|
|
terms := r.FormValue("terms")
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-14 23:09:27 +00:00
|
|
|
log.Info().Str("username", username).Str("name", name).Str("password", strings.Repeat("*", len(password))).Msg("Signup")
|
2025-11-06 22:31:51 +00:00
|
|
|
|
2025-11-05 14:15:06 +00:00
|
|
|
if terms != "on" {
|
2025-11-14 23:09:27 +00:00
|
|
|
log.Warn().Msg("Terms not agreed")
|
2025-11-05 14:15:06 +00:00
|
|
|
http.Error(w, "You must agree to the terms to register", http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 15:06:16 +00:00
|
|
|
user, err := auth.SignupUser(r.Context(), username, name, password)
|
2025-11-05 17:15:33 +00:00
|
|
|
if err != nil {
|
2025-11-05 14:15:06 +00:00
|
|
|
respondError(w, "Failed to signup user", err, http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-16 16:37:53 +00:00
|
|
|
auth.AddUserSession(r, user)
|
2025-11-05 17:15:33 +00:00
|
|
|
|
2025-11-05 14:20:56 +00:00
|
|
|
http.Redirect(w, r, "/", http.StatusFound)
|
2025-11-04 23:21:13 +00:00
|
|
|
}
|
2025-12-11 00:30:24 +00:00
|
|
|
|
|
|
|
|
func renderMock(templateName string) http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
code := chi.URLParam(r, "code")
|
|
|
|
|
if code == "" {
|
|
|
|
|
code = "abc-123"
|
|
|
|
|
}
|
|
|
|
|
htmlMock(templateName, w, code)
|
|
|
|
|
}
|
|
|
|
|
}
|