Sw stuff
This commit is contained in:
@@ -603,13 +603,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request, _ *visitor
|
|||||||
return s.writeJSON(w, response)
|
return s.writeJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request, v *visitor) error {
|
func (s *Server) handleConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
w.Header().Set("Cache-Control", "no-cache")
|
||||||
return s.writeJSON(w, s.configResponse(v))
|
return s.writeJSON(w, s.configResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, v *visitor) error {
|
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
b, err := json.MarshalIndent(s.configResponse(v), "", " ")
|
b, err := json.MarshalIndent(s.configResponse(), "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -619,10 +619,10 @@ func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, v *visi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) configResponse(v *visitor) *apiConfigResponse {
|
func (s *Server) configResponse() *apiConfigResponse {
|
||||||
authUser := ""
|
authMode := ""
|
||||||
if s.config.AuthUserHeader != "" && v != nil && v.User() != nil {
|
if s.config.AuthUserHeader != "" {
|
||||||
authUser = v.User().Name
|
authMode = "proxy"
|
||||||
}
|
}
|
||||||
return &apiConfigResponse{
|
return &apiConfigResponse{
|
||||||
BaseURL: "", // Will translate to window.location.origin
|
BaseURL: "", // Will translate to window.location.origin
|
||||||
@@ -639,8 +639,8 @@ func (s *Server) configResponse(v *visitor) *apiConfigResponse {
|
|||||||
WebPushPublicKey: s.config.WebPushPublicKey,
|
WebPushPublicKey: s.config.WebPushPublicKey,
|
||||||
DisallowedTopics: s.config.DisallowedTopics,
|
DisallowedTopics: s.config.DisallowedTopics,
|
||||||
ConfigHash: s.config.Hash(),
|
ConfigHash: s.config.Hash(),
|
||||||
|
AuthMode: authMode,
|
||||||
AuthLogoutURL: s.config.AuthLogoutURL,
|
AuthLogoutURL: s.config.AuthLogoutURL,
|
||||||
AuthUser: authUser,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -483,8 +483,8 @@ type apiConfigResponse struct {
|
|||||||
WebPushPublicKey string `json:"web_push_public_key"`
|
WebPushPublicKey string `json:"web_push_public_key"`
|
||||||
DisallowedTopics []string `json:"disallowed_topics"`
|
DisallowedTopics []string `json:"disallowed_topics"`
|
||||||
ConfigHash string `json:"config_hash"`
|
ConfigHash string `json:"config_hash"`
|
||||||
|
AuthMode string `json:"auth_mode,omitempty"` // "proxy" if auth-user-header is set, empty otherwise
|
||||||
AuthLogoutURL string `json:"auth_logout_url,omitempty"` // URL to redirect to on logout (only for proxy auth)
|
AuthLogoutURL string `json:"auth_logout_url,omitempty"` // URL to redirect to on logout (only for proxy auth)
|
||||||
AuthUser string `json:"auth_user,omitempty"` // Authenticated username (for proxy auth, empty if not using proxy auth)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type apiAccountBillingPrices struct {
|
type apiAccountBillingPrices struct {
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ var config = {
|
|||||||
web_push_public_key: "",
|
web_push_public_key: "",
|
||||||
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"],
|
disallowed_topics: ["docs", "static", "file", "app", "account", "settings", "signup", "login", "v1"],
|
||||||
config_hash: "dev", // Placeholder for development; actual value is generated server-side
|
config_hash: "dev", // Placeholder for development; actual value is generated server-side
|
||||||
|
auth_mode: "", // "proxy" if auth-user-header is set, empty otherwise
|
||||||
auth_logout_url: "", // URL to redirect to on logout (only for proxy auth)
|
auth_logout_url: "", // URL to redirect to on logout (only for proxy auth)
|
||||||
auth_user: "", // Authenticated username (non-empty if using proxy auth)
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ if (!import.meta.env.DEV) {
|
|||||||
// since we don't have access to `window` like in `src/app/config.js`
|
// since we don't have access to `window` like in `src/app/config.js`
|
||||||
self.importScripts("/config.js");
|
self.importScripts("/config.js");
|
||||||
|
|
||||||
// this is the fallback single-page-app route, matching vite.config.js PWA config,
|
// This is the fallback single-page-app route, matching vite.config.js PWA config,
|
||||||
// and is served by the go web server. It is needed for the single-page-app to work.
|
// and is served by the go web server. It is needed for the single-page-app to work.
|
||||||
// https://developer.chrome.com/docs/workbox/modules/workbox-routing/#how-to-register-a-navigation-route
|
// https://developer.chrome.com/docs/workbox/modules/workbox-routing/#how-to-register-a-navigation-route
|
||||||
registerRoute(
|
registerRoute(
|
||||||
@@ -354,10 +354,46 @@ if (!import.meta.env.DEV) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// the manifest excludes config.js (see vite.config.js) since the dist-file differs from the
|
// The manifest excludes config.js (see vite.config.js) since the dist-file differs from the
|
||||||
// actual config served by the go server. this adds it back with `NetworkFirst`, so that the
|
// actual config served by the go server. This adds it back with a custom handler that validates
|
||||||
// most recent config from the go server is cached, but the app still works if the network
|
// the response. If the response is HTML (e.g., from an auth proxy like Authelia), the service
|
||||||
// is unavailable. this is important since there's no "refresh" button in the installed pwa
|
// worker unregisters itself and clears caches to allow the auth proxy to handle the request.
|
||||||
// to force a reload.
|
registerRoute(
|
||||||
registerRoute(({ url }) => url.pathname === "/config.js", new NetworkFirst());
|
({ url }) => url.pathname === "/config.js",
|
||||||
|
async ({ request }) => {
|
||||||
|
const cache = await caches.open("config-cache");
|
||||||
|
try {
|
||||||
|
const response = await fetch(request);
|
||||||
|
const contentType = response.headers.get("content-type") || "";
|
||||||
|
|
||||||
|
// If we got HTML instead of JavaScript, we're likely logged out at the proxy level
|
||||||
|
// (e.g., Authelia is serving its login page). Clear caches, unregister the service
|
||||||
|
// worker, and reload all clients so the browser can handle the auth flow properly.
|
||||||
|
if (contentType.includes("text/html")) {
|
||||||
|
console.log("[ServiceWorker] Config returned HTML - proxy session likely expired, unregistering");
|
||||||
|
const cacheNames = await caches.keys();
|
||||||
|
await Promise.all(cacheNames.map((name) => caches.delete(name)));
|
||||||
|
await self.registration.unregister();
|
||||||
|
|
||||||
|
// Reload all open clients so they go through the auth proxy
|
||||||
|
const allClients = await self.clients.matchAll({ type: "window" });
|
||||||
|
allClients.forEach((client) => client.navigate(client.url));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid config response - cache it and return
|
||||||
|
await cache.put(request, response.clone());
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
// Network failed, try to serve from cache
|
||||||
|
console.log("[ServiceWorker] Network failed for config.js, trying cache", e);
|
||||||
|
const cached = await cache.match(request);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,10 +342,16 @@ class AccountApi {
|
|||||||
|
|
||||||
async sync() {
|
async sync() {
|
||||||
try {
|
try {
|
||||||
// For proxy auth, store the username from config if not already in session
|
// For proxy auth, detect user from /v1/account if no session exists
|
||||||
if (config.auth_user && !session.exists()) {
|
if (config.auth_mode === AuthMode.PROXY && !session.exists()) {
|
||||||
console.log(`[AccountApi] Proxy auth: storing session for user ${config.auth_user}`);
|
console.log(`[AccountApi] Proxy auth mode, detecting user from /v1/account`);
|
||||||
await session.store(config.auth_user, ""); // Empty token for proxy auth
|
const account = await this.get();
|
||||||
|
// Never store "*" (anonymous) as username
|
||||||
|
if (account.username && account.username !== "*") {
|
||||||
|
console.log(`[AccountApi] Proxy auth: storing session for ${account.username}`);
|
||||||
|
await session.store(account.username, ""); // Empty token for proxy auth
|
||||||
|
}
|
||||||
|
return account;
|
||||||
}
|
}
|
||||||
if (!session.exists()) {
|
if (!session.exists()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -373,6 +379,11 @@ class AccountApi {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`[AccountApi] Error fetching account`, e);
|
console.log(`[AccountApi] Error fetching account`, e);
|
||||||
if (e instanceof UnauthorizedError) {
|
if (e instanceof UnauthorizedError) {
|
||||||
|
// For proxy auth, hard refresh to get fresh auth from proxy
|
||||||
|
if (config.auth_mode === AuthMode.PROXY) {
|
||||||
|
window.location.reload();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
await session.resetAndRedirect(routes.login);
|
await session.resetAndRedirect(routes.login);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -437,5 +448,10 @@ export const Permission = {
|
|||||||
DENY_ALL: "deny-all",
|
DENY_ALL: "deny-all",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Maps to apiConfigResponse.AuthMode in server/types.go
|
||||||
|
export const AuthMode = {
|
||||||
|
PROXY: "proxy",
|
||||||
|
};
|
||||||
|
|
||||||
const accountApi = new AccountApi();
|
const accountApi = new AccountApi();
|
||||||
export default accountApi;
|
export default accountApi;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import routes from "./routes";
|
|||||||
import db from "../app/db";
|
import db from "../app/db";
|
||||||
import { topicDisplayName } from "../app/utils";
|
import { topicDisplayName } from "../app/utils";
|
||||||
import Navigation from "./Navigation";
|
import Navigation from "./Navigation";
|
||||||
import accountApi from "../app/AccountApi";
|
import accountApi, { AuthMode } from "../app/AccountApi";
|
||||||
import PopupMenu from "./PopupMenu";
|
import PopupMenu from "./PopupMenu";
|
||||||
import { SubscriptionPopup } from "./SubscriptionPopup";
|
import { SubscriptionPopup } from "./SubscriptionPopup";
|
||||||
import { useIsLaunchedPWA } from "./hooks";
|
import { useIsLaunchedPWA } from "./hooks";
|
||||||
@@ -140,7 +140,7 @@ const ProfileIcon = () => {
|
|||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
// For proxy auth, redirect to the logout URL if configured
|
// For proxy auth, redirect to the logout URL if configured
|
||||||
if (config.auth_user) {
|
if (config.auth_mode === AuthMode.PROXY) {
|
||||||
if (config.auth_logout_url) {
|
if (config.auth_logout_url) {
|
||||||
await db().delete();
|
await db().delete();
|
||||||
localStorage.removeItem("user");
|
localStorage.removeItem("user");
|
||||||
@@ -159,7 +159,7 @@ const ProfileIcon = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Determine if logout button should be shown (hide if proxy auth without logout URL)
|
// Determine if logout button should be shown (hide if proxy auth without logout URL)
|
||||||
const showLogout = !config.auth_user || config.auth_logout_url;
|
const showLogout = config.auth_mode !== AuthMode.PROXY || config.auth_logout_url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import WarningAmberIcon from "@mui/icons-material/WarningAmber";
|
|||||||
import { NavLink } from "react-router-dom";
|
import { NavLink } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
import { Visibility, VisibilityOff } from "@mui/icons-material";
|
||||||
import accountApi from "../app/AccountApi";
|
import accountApi, { AuthMode } from "../app/AccountApi";
|
||||||
import AvatarBox from "./AvatarBox";
|
import AvatarBox from "./AvatarBox";
|
||||||
import session from "../app/Session";
|
import session from "../app/Session";
|
||||||
import routes from "./routes";
|
import routes from "./routes";
|
||||||
@@ -20,7 +20,7 @@ const Login = () => {
|
|||||||
|
|
||||||
// Redirect to app if using proxy authentication
|
// Redirect to app if using proxy authentication
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config.auth_user) {
|
if (config.auth_mode === AuthMode.PROXY) {
|
||||||
window.location.href = routes.app;
|
window.location.href = routes.app;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user