Require Gitea login to write GPX data; IPv6-capable bind
Upload/delete/move/create-folder now require logging in via Gitea OAuth2 (only MAPTOOL_ALLOWED_GITEA_USER is granted write access); reading stays public. Deploy secrets (session secret, OAuth2 client credentials, allowed user) are written to .env from Gitea Actions repo secrets on every deploy instead of living on the server by hand. Also switch the container's bind address to "[::]" (dual-stack) so the app is reachable over IPv6, not just IPv4.
This commit is contained in:
1 parent
ae67cc72c7
commit
e13d1a1e97
13 files changed
+261
-11
No files matched your search
@@ -1,5 +1,7 @@
|
||||
require "sinatra/base"
|
||||
require "sinatra/json"
|
||||
require "oauth2"
|
||||
require "securerandom"
|
||||
require_relative "lib/maptool/gpx"
|
||||
require_relative "lib/maptool/gpx_store"
|
||||
require_relative "lib/maptool/gpsd_client"
|
||||
@@ -9,9 +11,73 @@ module Maptool
|
||||
set :public_folder, File.join(__dir__, "public")
|
||||
set :views, File.join(__dir__, "views")
|
||||
|
||||
# A persistent secret across restarts/redeploys — otherwise every deploy
|
||||
# would invalidate every logged-in session's cookie (see
|
||||
# docs/deployment.adoc). Falls back to a random one so the app still
|
||||
# loads locally/in CI without it set; that fallback just means sessions
|
||||
# don't survive a restart there, which is fine outside production.
|
||||
set :sessions, httponly: true, secure: production?, same_site: :lax,
|
||||
secret: ENV.fetch("SESSION_SECRET") { SecureRandom.hex(32) }
|
||||
|
||||
OFFLINE_PMTILES_PATH = File.join(__dir__, "data", "offline.pmtiles")
|
||||
GPX_STORE = GpxStore.new(File.join(__dir__, "data", "gpx"))
|
||||
|
||||
# Login delegates to the Gitea instance the user already has an account
|
||||
# on, rather than maintaining a local username/password — see
|
||||
# docs/architecture.adoc. Only ALLOWED_GITEA_USER may write; anyone else
|
||||
# can authenticate against Gitea but is still refused. No defaults for
|
||||
# the OAuth2 client id/secret/user: blank values just mean login (and
|
||||
# thus writing) doesn't work, which is fine outside production — nothing
|
||||
# here is eagerly validated at load time.
|
||||
GITEA_URL = ENV.fetch("GITEA_URL", "https://git.priesol.net")
|
||||
ALLOWED_GITEA_USER = ENV.fetch("MAPTOOL_ALLOWED_GITEA_USER", "")
|
||||
OAUTH_CLIENT = OAuth2::Client.new(
|
||||
ENV.fetch("GITEA_OAUTH_CLIENT_ID", ""),
|
||||
ENV.fetch("GITEA_OAUTH_CLIENT_SECRET", ""),
|
||||
site: GITEA_URL,
|
||||
authorize_url: "/login/oauth/authorize",
|
||||
token_url: "/login/oauth/access_token"
|
||||
)
|
||||
|
||||
helpers do
|
||||
def logged_in?
|
||||
!!session[:user]
|
||||
end
|
||||
|
||||
def require_login!
|
||||
halt 401, json(error: "login required", login_url: "/login") unless logged_in?
|
||||
end
|
||||
|
||||
def callback_url
|
||||
"#{request.base_url}/auth/callback"
|
||||
end
|
||||
end
|
||||
|
||||
get "/login" do
|
||||
session[:oauth_state] = SecureRandom.hex(16)
|
||||
redirect OAUTH_CLIENT.auth_code.authorize_url(
|
||||
redirect_uri: callback_url,
|
||||
state: session[:oauth_state],
|
||||
scope: "read:user"
|
||||
)
|
||||
end
|
||||
|
||||
get "/auth/callback" do
|
||||
halt 400, "invalid state" unless params[:state] && params[:state] == session.delete(:oauth_state)
|
||||
|
||||
token = OAUTH_CLIENT.auth_code.get_token(params[:code], redirect_uri: callback_url)
|
||||
user = token.get("/api/v1/user").parsed
|
||||
halt 403, "not authorized" unless user["login"] == ALLOWED_GITEA_USER
|
||||
|
||||
session[:user] = user["login"]
|
||||
redirect "/"
|
||||
end
|
||||
|
||||
get "/logout" do
|
||||
session.clear
|
||||
redirect "/"
|
||||
end
|
||||
|
||||
# Live GNSS position, polled by the frontend from /api/gnss/position. A
|
||||
# single background thread reads gpsd (if it's running — harmless retry
|
||||
# loop if not, see GpsdClient) and keeps this updated; polling instead
|
||||
@@ -41,7 +107,7 @@ module Maptool
|
||||
end
|
||||
|
||||
get "/" do
|
||||
erb :index
|
||||
erb :index, locals: { logged_in: logged_in?, user: session[:user] }
|
||||
end
|
||||
|
||||
get "/api/health" do
|
||||
@@ -66,6 +132,7 @@ module Maptool
|
||||
end
|
||||
|
||||
post "/api/gpx" do
|
||||
require_login!
|
||||
file = params[:file]
|
||||
halt 400, json(error: "file is required") unless file && file[:tempfile]
|
||||
|
||||
@@ -94,6 +161,7 @@ module Maptool
|
||||
end
|
||||
|
||||
post "/api/gpx/folder" do
|
||||
require_login!
|
||||
begin
|
||||
GPX_STORE.create_folder(params[:path].to_s)
|
||||
rescue GpxStore::InvalidPath
|
||||
@@ -104,6 +172,7 @@ module Maptool
|
||||
end
|
||||
|
||||
delete "/api/gpx/file/*" do
|
||||
require_login!
|
||||
begin
|
||||
GPX_STORE.delete_file(params[:splat].first)
|
||||
rescue GpxStore::InvalidPath
|
||||
@@ -116,6 +185,7 @@ module Maptool
|
||||
end
|
||||
|
||||
delete "/api/gpx/folder/*" do
|
||||
require_login!
|
||||
begin
|
||||
GPX_STORE.delete_folder(params[:splat].first)
|
||||
rescue GpxStore::InvalidPath
|
||||
@@ -128,6 +198,7 @@ module Maptool
|
||||
end
|
||||
|
||||
post "/api/gpx/move" do
|
||||
require_login!
|
||||
begin
|
||||
path = GPX_STORE.move(from: params[:from].to_s, to_folder: params[:to_folder].to_s)
|
||||
rescue GpxStore::InvalidPath
|
||||
|
||||
Reference in new issue
Block a user