web: Integrate BasePath support into frontend and fix web route serving
This commit is contained in:
parent
407e90a6d1
commit
945b916d55
35 changed files with 341 additions and 255 deletions
|
|
@ -58,7 +58,7 @@ func NewLoginController(authService happydns.AuthenticationUsecase, captchaVerif
|
|||
// @Security securitydefinitions.basic
|
||||
// @Success 200 {object} happydns.User
|
||||
// @Failure 401 {object} happydns.ErrorResponse "Authentication failure"
|
||||
// @Router /auth/user [get]
|
||||
// @Router /auth [get]
|
||||
func (lc *LoginController) GetLoggedUser(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, c.MustGet("LoggedUser"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,10 @@ func ConsolidateConfig() (opts *happydns.Options, err error) {
|
|||
} else {
|
||||
opts.BasePath = ""
|
||||
}
|
||||
if opts.DevProxy != "" && opts.BasePath != "" {
|
||||
err = fmt.Errorf("-base-path is not supported in -dev mode")
|
||||
return
|
||||
}
|
||||
|
||||
if opts.NoMail && opts.MailSMTPHost != "" {
|
||||
err = fmt.Errorf("-no-mail and -mail-smtp-* cannot be defined at the same time")
|
||||
|
|
|
|||
248
web/routes.go
248
web/routes.go
|
|
@ -22,6 +22,7 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
|
|
@ -38,7 +39,6 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
indexTpl *template.Template
|
||||
CustomHeadHTML = ""
|
||||
CustomBodyHTML = ""
|
||||
HideVoxPeople = false
|
||||
|
|
@ -99,107 +99,136 @@ func DeclareRoutes(cfg *happydns.Options, router *gin.RouterGroup, captchaVerifi
|
|||
CustomHeadHTML += `<script id="app-config" type="application/json">` + string(appcfg) + `</script>`
|
||||
}
|
||||
|
||||
if cfg.DevProxy != "" {
|
||||
router.GET("/.svelte-kit/*_", serveOrReverse("", cfg))
|
||||
router.GET("/node_modules/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@vite/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@id/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@fs/*_", serveOrReverse("", cfg))
|
||||
router.GET("/src/*_", serveOrReverse("", cfg))
|
||||
router.GET("/home/*_", serveOrReverse("", cfg))
|
||||
}
|
||||
router.GET("/_app/*_", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg))
|
||||
serveFile := serveOrReverse("", cfg)
|
||||
serveIndex := serveOrReverse("/", cfg)
|
||||
serveManifest := serveOrReverse("/manifest.json", cfg)
|
||||
immutable := func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }
|
||||
|
||||
router.GET("/", serveOrReverse("/", cfg))
|
||||
router.GET("/index.html", serveOrReverse("/", cfg))
|
||||
if cfg.DevProxy != "" {
|
||||
router.GET("/.svelte-kit/*_", serveFile)
|
||||
router.GET("/node_modules/*_", serveFile)
|
||||
router.GET("/@vite/*_", serveFile)
|
||||
router.GET("/@id/*_", serveFile)
|
||||
router.GET("/@fs/*_", serveFile)
|
||||
router.GET("/src/*_", serveFile)
|
||||
router.GET("/home/*_", serveFile)
|
||||
}
|
||||
router.GET("/_app/*_", immutable, serveFile)
|
||||
|
||||
router.GET("/", serveIndex)
|
||||
router.GET("/index.html", serveIndex)
|
||||
|
||||
// Routes handled by the showcase
|
||||
router.GET("/en/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/fr/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/en/*_", serveIndex)
|
||||
router.GET("/fr/*_", serveIndex)
|
||||
|
||||
// Routes for real existings files
|
||||
router.GET("/fonts/*path", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg))
|
||||
router.GET("/img/*path", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg))
|
||||
router.GET("/favicon.ico", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg))
|
||||
router.GET("/manifest.json", serveOrReverse("/manifest.json", cfg))
|
||||
router.GET("/robots.txt", serveOrReverse("", cfg))
|
||||
router.GET("/service-worker.js", serveOrReverse("", cfg))
|
||||
router.GET("/fonts/*path", immutable, serveFile)
|
||||
router.GET("/img/*path", immutable, serveFile)
|
||||
router.GET("/favicon.ico", immutable, serveFile)
|
||||
router.GET("/manifest.json", serveManifest)
|
||||
router.GET("/robots.txt", serveFile)
|
||||
router.GET("/service-worker.js", serveFile)
|
||||
|
||||
// Routes to virtual content
|
||||
router.GET("/domains/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/email-validation", serveOrReverse("/", cfg))
|
||||
router.GET("/forgotten-password", serveOrReverse("/", cfg))
|
||||
router.GET("/join", serveOrReverse("/", cfg))
|
||||
router.GET("/login", serveOrReverse("/", cfg))
|
||||
router.GET("/me", serveOrReverse("/", cfg))
|
||||
router.GET("/onboarding/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/providers/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/services/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/tools/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/resolver/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/zones/*_", serveOrReverse("/", cfg))
|
||||
router.GET("/domains/*_", serveIndex)
|
||||
router.GET("/email-validation", serveIndex)
|
||||
router.GET("/forgotten-password", serveIndex)
|
||||
router.GET("/join", serveIndex)
|
||||
router.GET("/login", serveIndex)
|
||||
router.GET("/me", serveIndex)
|
||||
router.GET("/onboarding/*_", serveIndex)
|
||||
router.GET("/providers/*_", serveIndex)
|
||||
router.GET("/services/*_", serveIndex)
|
||||
router.GET("/tools/*_", serveIndex)
|
||||
router.GET("/resolver/*_", serveIndex)
|
||||
router.GET("/zones/*_", serveIndex)
|
||||
}
|
||||
|
||||
func NoRoute(cfg *happydns.Options, router *gin.Engine) {
|
||||
serveIndex := serveOrReverse("/", cfg)
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.URL.Path, cfg.BasePath+"/api") || strings.Contains(c.Request.Header.Get("Accept"), "application/json") {
|
||||
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "errmsg": "Page not found"})
|
||||
} else if cfg.BasePath != "" && !strings.HasPrefix(c.Request.URL.Path, cfg.BasePath) {
|
||||
c.Redirect(http.StatusFound, cfg.BasePath+c.Request.URL.Path)
|
||||
} else {
|
||||
serveOrReverse("/", cfg)(c)
|
||||
serveIndex(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func serveOrReverse(forced_url string, cfg *happydns.Options) gin.HandlerFunc {
|
||||
if cfg.DevProxy != "" {
|
||||
// Parse once at creation time, not per request
|
||||
devURL, err := url.Parse(cfg.DevProxy)
|
||||
if err != nil {
|
||||
return func(c *gin.Context) {
|
||||
http.Error(c.Writer, "invalid dev proxy URL: "+err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward to the Vue dev proxy
|
||||
return func(c *gin.Context) {
|
||||
if u, err := url.Parse(cfg.DevProxy); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
u := *devURL // copy to avoid mutating shared state across requests
|
||||
if forced_url != "" {
|
||||
u.Path = path.Join(u.Path, forced_url)
|
||||
} else {
|
||||
if forced_url != "" {
|
||||
u.Path = path.Join(u.Path, forced_url)
|
||||
} else {
|
||||
u.Path = path.Join(u.Path, c.Request.URL.Path)
|
||||
u.Path = path.Join(u.Path, c.Request.URL.Path)
|
||||
}
|
||||
u.RawQuery = c.Request.URL.RawQuery
|
||||
|
||||
r, err := http.NewRequest(c.Request.Method, u.String(), c.Request.Body)
|
||||
if err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if u.Path != "/" || resp.StatusCode != 200 {
|
||||
for key, vals := range resp.Header {
|
||||
for _, v := range vals {
|
||||
c.Writer.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
|
||||
u.RawQuery = c.Request.URL.RawQuery
|
||||
|
||||
if r, err := http.NewRequest(c.Request.Method, u.String(), c.Request.Body); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
} else if resp, err := http.DefaultClient.Do(r); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusBadGateway)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
|
||||
if u.Path != "/" || resp.StatusCode != 200 {
|
||||
for key := range resp.Header {
|
||||
c.Writer.Header().Add(key, resp.Header.Get(key))
|
||||
}
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
} else {
|
||||
for key := range resp.Header {
|
||||
if strings.ToLower(key) != "content-length" {
|
||||
c.Writer.Header().Add(key, resp.Header.Get(key))
|
||||
}
|
||||
}
|
||||
|
||||
v, _ := io.ReadAll(resp.Body)
|
||||
|
||||
v2 := strings.Replace(strings.Replace(string(v), "</head>", "{{ .Head }}</head>", 1), "</body>", "{{ .Body }}</body>", 1)
|
||||
|
||||
indexTpl = template.Must(template.New("index.html").Parse(v2))
|
||||
|
||||
if err := indexTpl.ExecuteTemplate(c.Writer, "index.html", map[string]string{
|
||||
"Body": CustomBodyHTML,
|
||||
"Head": CustomHeadHTML,
|
||||
}); err != nil {
|
||||
log.Println("Unable to return index.html:", err.Error())
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
} else {
|
||||
for key, vals := range resp.Header {
|
||||
if !strings.EqualFold(key, "content-length") {
|
||||
for _, v := range vals {
|
||||
c.Writer.Header().Add(key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
// Local template per request — no race condition on a package-level var
|
||||
tpl, err := template.New("index.html").Parse(
|
||||
strings.Replace(strings.Replace(string(v),
|
||||
"</head>", "{{ .Head }}</head>", 1),
|
||||
"</body>", "{{ .Body }}</body>", 1),
|
||||
)
|
||||
if err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := tpl.Execute(c.Writer, map[string]string{
|
||||
"Body": CustomBodyHTML,
|
||||
"Head": CustomHeadHTML,
|
||||
}); err != nil {
|
||||
log.Println("Unable to return index.html:", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if Assets == nil {
|
||||
|
|
@ -207,31 +236,60 @@ func serveOrReverse(forced_url string, cfg *happydns.Options) gin.HandlerFunc {
|
|||
c.String(http.StatusNotFound, "404 Page not found - interface not embedded in binary, please compile with -tags web")
|
||||
}
|
||||
} else if forced_url == "/" {
|
||||
// Serve altered index.html
|
||||
// Pre-render index.html once at handler creation time
|
||||
f, err := Assets.Open("index.html")
|
||||
if err != nil {
|
||||
log.Println("Unable to open embedded index.html:", err)
|
||||
return func(c *gin.Context) {
|
||||
c.String(http.StatusInternalServerError, "index.html not found in embedded assets")
|
||||
}
|
||||
}
|
||||
v, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Println("Unable to read embedded index.html:", err)
|
||||
return func(c *gin.Context) {
|
||||
c.String(http.StatusInternalServerError, "failed to read embedded index.html")
|
||||
}
|
||||
}
|
||||
|
||||
rendered := []byte(strings.Replace(strings.Replace(string(v), "</head>", CustomHeadHTML+"</head>", 1), "</body>", CustomBodyHTML+"</body>", 1))
|
||||
|
||||
if cfg.BasePath != "" {
|
||||
rendered = bytes.ReplaceAll(
|
||||
bytes.ReplaceAll(
|
||||
bytes.ReplaceAll(
|
||||
bytes.ReplaceAll(
|
||||
rendered,
|
||||
[]byte(`href="/`),
|
||||
append([]byte(`href="`), append([]byte(cfg.BasePath), '/')...),
|
||||
),
|
||||
[]byte(`import("/`),
|
||||
append([]byte(`import("`), append([]byte(cfg.BasePath), '/')...),
|
||||
),
|
||||
[]byte(`base: "`),
|
||||
append([]byte(`base: "`), []byte(cfg.BasePath)...),
|
||||
),
|
||||
[]byte("</head>"),
|
||||
[]byte(`<base href="`+cfg.BasePath+`"></head>`),
|
||||
)
|
||||
}
|
||||
|
||||
return func(c *gin.Context) {
|
||||
if indexTpl == nil {
|
||||
// Create template from file
|
||||
f, _ := Assets.Open("index.html")
|
||||
v, _ := io.ReadAll(f)
|
||||
|
||||
v2 := strings.Replace(strings.Replace(string(v), "</head>", "{{ .Head }}</head>", 1), "</body>", "{{ .Body }}</body>", 1)
|
||||
|
||||
indexTpl = template.Must(template.New("index.html").Parse(v2))
|
||||
}
|
||||
|
||||
// Serve template
|
||||
if err := indexTpl.ExecuteTemplate(c.Writer, "index.html", map[string]string{
|
||||
"Body": CustomBodyHTML,
|
||||
"Head": CustomHeadHTML,
|
||||
}); err != nil {
|
||||
log.Println("Unable to return index.html:", err.Error())
|
||||
}
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", rendered)
|
||||
}
|
||||
} else if forced_url == "/manifest.json" {
|
||||
// Serve altered manifest.json
|
||||
return func(c *gin.Context) {
|
||||
f, _ := Assets.Open("manifest.json")
|
||||
v, _ := io.ReadAll(f)
|
||||
f, err := Assets.Open("manifest.json")
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "manifest.json not found in embedded assets")
|
||||
return
|
||||
}
|
||||
v, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "failed to read manifest.json")
|
||||
return
|
||||
}
|
||||
v2 := strings.Replace(strings.Replace(string(v), "\"id\": \"/\"", "\"id\": \""+cfg.BasePath+"\"", 1), "\"start_url\": \"/\"", "\"start_url\": \""+cfg.BasePath+"\"", 1)
|
||||
|
||||
c.Data(http.StatusOK, "application/manifest+json", []byte(v2))
|
||||
|
|
|
|||
27
web/src/lib/api/auth.ts
Normal file
27
web/src/lib/api/auth.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// This file is part of the happyDomain (R) project.
|
||||
// Copyright (c) 2022-2026 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// For AGPL licensing:
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { base } from "$lib/stores/config";
|
||||
|
||||
export async function getOidcProvider(): Promise<{ provider: string }> {
|
||||
const res = await fetch(`${base}/auth/has_oidc`);
|
||||
return res.json();
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import {
|
|||
postUsersByUserIdSettings,
|
||||
getUsersByUserId,
|
||||
} from "$lib/api-base/sdk.gen";
|
||||
import { client } from "$lib/api-base/client.gen";
|
||||
import type { UserSettings } from "$lib/model/usersettings";
|
||||
import type { User, SignUpForm, LoginForm } from "$lib/model/user";
|
||||
import { unwrapSdkResponse, unwrapEmptyResponse } from "./errors";
|
||||
|
|
@ -44,43 +45,6 @@ export async function authUser(form: LoginForm): Promise<User> {
|
|||
return unwrapSdkResponse(await postAuth({ body: form })) as unknown as User;
|
||||
}
|
||||
|
||||
export class CaptchaRequiredError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "CaptchaRequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
export async function authUserWithCaptcha(form: LoginForm): Promise<User> {
|
||||
// Use raw fetch to bypass the customFetch session-refresh wrapper,
|
||||
// which would consume the 401 response before we can inspect captcha_required.
|
||||
const baseUrl = typeof window !== 'undefined' ? "/api/" : "http://localhost/api/";
|
||||
const response = await fetch(`${baseUrl}auth`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(form),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let body: Record<string, unknown> = {};
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (body.captcha_required) {
|
||||
throw new CaptchaRequiredError(
|
||||
typeof body.errmsg === 'string' ? body.errmsg : "Captcha verification required."
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error(typeof body.errmsg === 'string' ? body.errmsg : "Invalid username or password.");
|
||||
}
|
||||
|
||||
return response.json() as Promise<User>;
|
||||
}
|
||||
|
||||
export async function logout(): Promise<boolean> {
|
||||
return unwrapEmptyResponse(await postAuthLogout());
|
||||
}
|
||||
|
|
@ -185,6 +149,14 @@ export function cleanUserSession(): void {
|
|||
}
|
||||
}
|
||||
|
||||
export async function isAuthUser(user: User): Promise<boolean> {
|
||||
const result = await client.get({
|
||||
url: "/users/{userId}/is_auth_user",
|
||||
path: { userId: user.id },
|
||||
} as any);
|
||||
return result.response?.status === 204;
|
||||
}
|
||||
|
||||
export async function getUser(id: string): Promise<User> {
|
||||
return unwrapSdkResponse(
|
||||
await getUsersByUserId({
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
import type { ClassValue } from "svelte/elements";
|
||||
|
||||
|
|
@ -41,7 +40,7 @@
|
|||
import { logout as APILogout } from "$lib/api/user";
|
||||
import HelpButton from "$lib/components/Help.svelte";
|
||||
import Logo from "$lib/components/Logo.svelte";
|
||||
import { appConfig } from "$lib/stores/config";
|
||||
import { appConfig, navigate } from "$lib/stores/config";
|
||||
import { providersSpecs } from "$lib/stores/providers";
|
||||
import { userSession, refreshUserSession } from "$lib/stores/usersession";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
|
|
@ -104,7 +103,7 @@
|
|||
refreshUserSession().then(
|
||||
() => {},
|
||||
() => {
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import {
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
}
|
||||
|
||||
if (!provider) {
|
||||
goto("/domains/new/" + encodeURIComponent(value));
|
||||
navigate("/domains/new/" + encodeURIComponent(value));
|
||||
} else {
|
||||
addDomain(value, provider).then(
|
||||
(domain) => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, Col, Container, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -58,14 +58,14 @@
|
|||
function createProviderForm(ptype: string, providerId: string | null, value: ProviderSettingsState | null, edit: boolean): ProviderForm {
|
||||
const pf = new ProviderForm(
|
||||
ptype,
|
||||
() => refreshProviders().then(() => goto("/?newProvider")),
|
||||
() => refreshProviders().then(() => navigate("/?newProvider")),
|
||||
providerId,
|
||||
value,
|
||||
() => {
|
||||
if (edit) {
|
||||
goto("/providers");
|
||||
navigate("/providers");
|
||||
} else {
|
||||
goto("/providers/new");
|
||||
navigate("/providers/new");
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -56,7 +56,7 @@
|
|||
if (actionAddDomain) {
|
||||
addDomainToProvider();
|
||||
} else if (filteredDomains.length > 0) {
|
||||
goto("/domains/" + encodeURIComponent(filteredDomains[0].id));
|
||||
navigate("/domains/" + encodeURIComponent(filteredDomains[0].id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
},
|
||||
);
|
||||
} else {
|
||||
goto("/domains/new/" + encodeURIComponent($filteredName));
|
||||
navigate("/domains/new/" + encodeURIComponent($filteredName));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
|
|
@ -33,7 +31,7 @@
|
|||
|
||||
import ProviderList from "$lib/components/providers/List.svelte";
|
||||
import type { Provider } from "$lib/model/provider";
|
||||
import { appConfig } from "$lib/stores/config";
|
||||
import { appConfig, navigate } from "$lib/stores/config";
|
||||
import {
|
||||
providers,
|
||||
providersSpecs,
|
||||
|
|
@ -67,7 +65,7 @@
|
|||
items={$providers}
|
||||
noLabel
|
||||
bind:selectedProvider={filteredProvider}
|
||||
on:new-provider={() => goto("/providers/new")}
|
||||
on:new-provider={() => navigate("/providers/new")}
|
||||
/>
|
||||
{/if}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import {
|
||||
|
|
@ -108,7 +108,7 @@
|
|||
|
||||
function updateProvider(event: Event, item: Provider) {
|
||||
event.stopPropagation();
|
||||
goto("/providers/" + encodeURIComponent(item._id));
|
||||
navigate("/providers/" + encodeURIComponent(item._id));
|
||||
}
|
||||
|
||||
async function delProvider(event: Event, item: Provider) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { base } from "$lib/stores/config";
|
||||
import { refreshUserSession } from "$lib/stores/usersession";
|
||||
import type { CreateClientConfig } from "./api-base/client.gen";
|
||||
|
||||
|
|
@ -29,6 +30,13 @@ export class NotAuthorizedError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class CaptchaRequiredError extends NotAuthorizedError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "CaptchaRequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ProviderNoDomainListingSupport extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
|
|
@ -39,8 +47,24 @@ export class ProviderNoDomainListingSupport extends Error {
|
|||
async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||
const response = await fetch(input, init);
|
||||
|
||||
// Handle 401 Unauthorized - attempt session refresh and retry
|
||||
// Handle 401 Unauthorized - check for captcha requirement first, then attempt session refresh
|
||||
if (response.status === 401) {
|
||||
if (response.headers.get("content-type")?.includes("application/json")) {
|
||||
const clone = response.clone();
|
||||
try {
|
||||
const json = await clone.json();
|
||||
if (json.captcha_required) {
|
||||
throw new CaptchaRequiredError(
|
||||
typeof json.errmsg === "string"
|
||||
? json.errmsg
|
||||
: "Captcha verification required.",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof CaptchaRequiredError) throw err;
|
||||
// ignore JSON parsing errors
|
||||
}
|
||||
}
|
||||
try {
|
||||
await refreshUserSession();
|
||||
// Retry the original request after successful session refresh
|
||||
|
|
@ -66,7 +90,7 @@ async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promis
|
|||
if (
|
||||
response.status === 400 &&
|
||||
json.error ===
|
||||
"error in openapi3filter.SecurityRequirementsError: security requirements failed: invalid session"
|
||||
"error in openapi3filter.SecurityRequirementsError: security requirements failed: invalid session"
|
||||
) {
|
||||
throw new NotAuthorizedError(json.error.substring(80));
|
||||
}
|
||||
|
|
@ -79,7 +103,10 @@ async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promis
|
|||
}
|
||||
} catch (err) {
|
||||
// If it's one of our custom errors, re-throw it
|
||||
if (err instanceof NotAuthorizedError || err instanceof ProviderNoDomainListingSupport) {
|
||||
if (
|
||||
err instanceof NotAuthorizedError ||
|
||||
err instanceof ProviderNoDomainListingSupport
|
||||
) {
|
||||
throw err;
|
||||
}
|
||||
// Otherwise, ignore JSON parsing errors and return the original response
|
||||
|
|
@ -92,7 +119,7 @@ async function customFetch(input: RequestInfo | URL, init?: RequestInit): Promis
|
|||
export const createClientConfig: CreateClientConfig = (config) => {
|
||||
// In test environments (Node.js), we need a full URL with protocol and host
|
||||
// In browser environments, relative URLs work fine
|
||||
const baseUrl = typeof window !== 'undefined' ? "/api/" : "http://localhost/api/";
|
||||
const baseUrl = typeof window !== "undefined" ? base + "/api/" : "http://localhost/api/";
|
||||
|
||||
return {
|
||||
...config,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
import type { Color } from "@sveltestrap/sveltestrap";
|
||||
|
|
@ -65,3 +66,11 @@ function getConfigFromScriptTag(): AppConfig | null {
|
|||
const initialConfig = getConfigFromScriptTag() || defaultConfig;
|
||||
|
||||
export const appConfig = writable<AppConfig>(initialConfig);
|
||||
|
||||
export const base: string = typeof document !== 'undefined'
|
||||
? (document.querySelector('base')?.getAttribute('href') ?? '')
|
||||
: '';
|
||||
|
||||
export function navigate(url: string, opts?: Parameters<typeof goto>[1]) {
|
||||
return goto(base + url, opts);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { getProvidersSpecs } from "$lib/api-base/sdk.gen";
|
||||
import { unwrapSdkResponse } from "$lib/api/errors";
|
||||
import { listProviders } from "$lib/api/provider";
|
||||
import type { Provider, ProviderInfos } from "$lib/model/provider";
|
||||
import { filteredProvider } from "$lib/stores/home";
|
||||
|
|
@ -62,12 +64,7 @@ export const providers_idx = derived(providers, ($providers: Array<Provider> | u
|
|||
});
|
||||
|
||||
export async function refreshProvidersSpecs() {
|
||||
const res = await fetch("/api/providers/_specs", { headers: { Accept: "application/json" } });
|
||||
if (res.status == 200) {
|
||||
const map = await res.json();
|
||||
providersSpecs.set(map);
|
||||
return map;
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
const map = unwrapSdkResponse(await getProvidersSpecs()) as Record<string, ProviderInfos>;
|
||||
providersSpecs.set(map);
|
||||
return map;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { getServiceSpecs } from "$lib/api-base/sdk.gen";
|
||||
import { unwrapSdkResponse } from "$lib/api/errors";
|
||||
import { derived, writable, type Writable } from "svelte/store";
|
||||
import type { ServiceInfos } from "$lib/model/service_specs.svelte";
|
||||
|
||||
|
|
@ -30,16 +32,15 @@ export async function refreshServicesSpecs() {
|
|||
servicesSpecsLoaded.set(false);
|
||||
servicesSpecsError.set(null);
|
||||
|
||||
const res = await fetch("/api/service_specs", { headers: { Accept: "application/json" } });
|
||||
if (res.status == 200) {
|
||||
const map = await res.json();
|
||||
try {
|
||||
const map = unwrapSdkResponse(await getServiceSpecs()) as Record<string, ServiceInfos>;
|
||||
servicesSpecs.set(map);
|
||||
servicesSpecsLoaded.set(true);
|
||||
return map;
|
||||
} else {
|
||||
const errmsg = (await res.json()).errmsg;
|
||||
} catch (err) {
|
||||
const errmsg = err instanceof Error ? err.message : String(err);
|
||||
servicesSpecsError.set(errmsg);
|
||||
throw new Error(errmsg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,23 +20,19 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { getAuth } from "$lib/api-base/sdk.gen";
|
||||
import { unwrapSdkResponse } from "$lib/api/errors";
|
||||
import type { User } from "$lib/model/user";
|
||||
|
||||
export const userSession: Writable<User> = writable({} as User);
|
||||
|
||||
export async function refreshUserSession(
|
||||
fetch: (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit | undefined,
|
||||
) => Promise<Response> = window.fetch,
|
||||
) {
|
||||
const res = await fetch("/api/auth", { headers: { Accept: "application/json" } });
|
||||
if (res.status == 200) {
|
||||
const user = (await res.json()) as User;
|
||||
export async function refreshUserSession() {
|
||||
try {
|
||||
const user = unwrapSdkResponse(await getAuth()) as unknown as User;
|
||||
userSession.set(user);
|
||||
return user;
|
||||
} else {
|
||||
} catch (err) {
|
||||
userSession.set({} as User);
|
||||
throw new Error((await res.json()).errmsg);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,12 +25,10 @@
|
|||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
import "../app.scss";
|
||||
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import Header from "$lib/components/Header.svelte";
|
||||
import Toaster from "$lib/components/Toaster.svelte";
|
||||
import VoxPeople from "$lib/components/VoxPeople.svelte";
|
||||
import { appConfig } from "$lib/stores/config";
|
||||
import { appConfig, navigate } from "$lib/stores/config";
|
||||
import { providers } from "$lib/stores/providers";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import { locale, t } from "$lib/translations";
|
||||
|
|
@ -53,7 +51,7 @@
|
|||
|
||||
window.onunhandledrejection = (e) => {
|
||||
if (e.reason.name == "NotAuthorizedError") {
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
providers.set(undefined);
|
||||
toasts.addErrorToast({
|
||||
title: $t("errors.session.title"),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function onSWupdate(sw_state: { hasUpdate: boolean }, installingWorker: ServiceW
|
|||
sw_state.hasUpdate = true;
|
||||
}
|
||||
|
||||
export const load: Load = async ({ fetch, route, url }) => {
|
||||
export const load: Load = async ({ route, url }) => {
|
||||
const { MODE } = import.meta.env;
|
||||
|
||||
const initLocale =
|
||||
|
|
@ -94,7 +94,7 @@ export const load: Load = async ({ fetch, route, url }) => {
|
|||
|
||||
// Load user session if any
|
||||
try {
|
||||
const user = await refreshUserSession(fetch);
|
||||
const user = await refreshUserSession();
|
||||
if (!url.searchParams.has("lang") && get(locale) != user.settings.language) {
|
||||
locale.set(user.settings.language);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto, invalidateAll } from "$app/navigation";
|
||||
import { invalidateAll } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { page } from "$app/state";
|
||||
|
||||
import {
|
||||
|
|
@ -73,7 +74,7 @@
|
|||
let selectedDomain = $derived(data.domain.id);
|
||||
function domainChange(dn: string) {
|
||||
if (dn != data.domain.id) {
|
||||
goto(
|
||||
navigate(
|
||||
"/domains/" +
|
||||
encodeURIComponent(domainLink(dn)) +
|
||||
(page.route.id
|
||||
|
|
@ -102,7 +103,7 @@
|
|||
retrievalInProgress = false;
|
||||
if (page.data.definedhistory) {
|
||||
refreshDomains().then(() => {
|
||||
goto(
|
||||
navigate(
|
||||
"/domains/" +
|
||||
encodeURIComponent(domainLink(selectedDomain)) +
|
||||
"/" +
|
||||
|
|
@ -130,11 +131,11 @@
|
|||
refreshDomains().then(
|
||||
() => {
|
||||
deleteInProgress = false;
|
||||
goto("/domains");
|
||||
navigate("/domains");
|
||||
},
|
||||
() => {
|
||||
deleteInProgress = false;
|
||||
goto("/domains");
|
||||
navigate("/domains");
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
let selectedHistory: string = $derived(data.history);
|
||||
function historyChange(history: string) {
|
||||
if (data.history != history) {
|
||||
goto(
|
||||
navigate(
|
||||
"/domains/" +
|
||||
encodeURIComponent(
|
||||
$domains_idx[data.domain.domain]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Alert, Icon, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
() => {
|
||||
refreshDomains().then(
|
||||
() => {
|
||||
goto(
|
||||
navigate(
|
||||
`/domains/${encodeURIComponent($domains_idx[data.domain.domain] ? data.domain.domain : data.domain.id)}`,
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, Col, Container, Icon, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
});
|
||||
|
||||
refreshDomains();
|
||||
goto("/domains/");
|
||||
navigate("/domains/");
|
||||
},
|
||||
(error) => {
|
||||
addingNewDomain = false;
|
||||
|
|
|
|||
|
|
@ -22,12 +22,10 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import { Alert, Col, Container, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { validateEmail } from "$lib/api/user";
|
||||
import { appConfig } from "$lib/stores/config";
|
||||
import { appConfig, navigate } from "$lib/stores/config";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import { t } from "$lib/translations";
|
||||
import EmailConfirmationForm from "./EmailConfirmationForm.svelte";
|
||||
|
|
@ -50,7 +48,7 @@
|
|||
timeout: 5000,
|
||||
type: "success",
|
||||
});
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
},
|
||||
(err) => {
|
||||
error = err;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, Col, Input, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
timeout: 5000,
|
||||
type: "success",
|
||||
});
|
||||
goto("/");
|
||||
navigate("/");
|
||||
},
|
||||
(error) => {
|
||||
formSent = false;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, Col, Input, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
timeout: 20000,
|
||||
color: "success",
|
||||
});
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
},
|
||||
(error) => {
|
||||
formSent = false;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, Col, Input, Row, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
},
|
||||
(error) => {
|
||||
formSent = false;
|
||||
|
|
|
|||
|
|
@ -22,15 +22,13 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
import { Button, FormGroup, Icon, Input, Label, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t, locale } from "$lib/translations";
|
||||
import { registerUser } from "$lib/api/user";
|
||||
import type { SignUpForm } from "$lib/model/user";
|
||||
import { checkWeakPassword, checkPasswordConfirmation } from "$lib/password";
|
||||
import { appConfig } from "$lib/stores/config";
|
||||
import { appConfig, navigate } from "$lib/stores/config";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import CaptchaWidget from "$lib/components/CaptchaWidget.svelte";
|
||||
|
||||
|
|
@ -73,7 +71,7 @@
|
|||
type: "success",
|
||||
timeout: 5000,
|
||||
});
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
},
|
||||
(error) => {
|
||||
formSent = false;
|
||||
|
|
|
|||
|
|
@ -22,15 +22,16 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { page } from "$app/state";
|
||||
|
||||
import { Button, FormGroup, Input, Label, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { t } from "$lib/translations";
|
||||
import { authUserWithCaptcha, CaptchaRequiredError, cleanUserSession } from "$lib/api/user";
|
||||
import { getOidcProvider } from "$lib/api/auth";
|
||||
import { authUser, cleanUserSession } from "$lib/api/user";
|
||||
import { CaptchaRequiredError } from "$lib/hey-api";
|
||||
import type { LoginForm } from "$lib/model/user";
|
||||
import { appConfig } from "$lib/stores/config";
|
||||
import { appConfig, navigate } from "$lib/stores/config";
|
||||
import { providers } from "$lib/stores/providers";
|
||||
import { toasts } from "$lib/stores/toasts";
|
||||
import { refreshUserSession } from "$lib/stores/usersession";
|
||||
|
|
@ -65,7 +66,7 @@
|
|||
? { ...loginForm, captcha_token: captchaToken }
|
||||
: loginForm;
|
||||
|
||||
authUserWithCaptcha(formWithCaptcha).then(
|
||||
authUser(formWithCaptcha).then(
|
||||
() => {
|
||||
cleanUserSession();
|
||||
providers.set(undefined);
|
||||
|
|
@ -76,9 +77,9 @@
|
|||
|
||||
const nextParam = page.url.searchParams.get("next");
|
||||
if (nextParam) {
|
||||
goto(decodeURIComponent(nextParam));
|
||||
navigate(decodeURIComponent(nextParam));
|
||||
} else {
|
||||
goto("/");
|
||||
navigate("/");
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -114,7 +115,7 @@
|
|||
timeout: 10000,
|
||||
});
|
||||
} else {
|
||||
goto("/forgotten-password");
|
||||
navigate("/forgotten-password");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -161,23 +162,21 @@
|
|||
{$t("common.go")}
|
||||
</Button>
|
||||
{#if $appConfig.oidc_configured}
|
||||
{#await fetch("/auth/has_oidc") then res}
|
||||
{#await res.json() then oidc}
|
||||
<Button href="/auth/oidc" color="secondary">
|
||||
{#if oidc.provider == "google.com"}
|
||||
<i class="bi bi-google"></i>
|
||||
{:else if oidc.provider == "gitlab.com" || oidc.provider == "framagit.org"}
|
||||
<i class="bi bi-gitlab"></i>
|
||||
{:else if oidc.provider == "github.com"}
|
||||
<i class="bi bi-github"></i>
|
||||
{:else if oidc.provider == "microsoft.com"}
|
||||
<i class="bi bi-microsoft"></i>
|
||||
{:else if oidc.provider == "apple.com"}
|
||||
<i class="bi bi-apple"></i>
|
||||
{/if}
|
||||
{$t("account.oidc-login", { provider: oidc.provider })}
|
||||
</Button>
|
||||
{/await}
|
||||
{#await getOidcProvider() then oidc}
|
||||
<Button href="/auth/oidc" color="secondary">
|
||||
{#if oidc.provider == "google.com"}
|
||||
<i class="bi bi-google"></i>
|
||||
{:else if oidc.provider == "gitlab.com" || oidc.provider == "framagit.org"}
|
||||
<i class="bi bi-gitlab"></i>
|
||||
{:else if oidc.provider == "github.com"}
|
||||
<i class="bi bi-github"></i>
|
||||
{:else if oidc.provider == "microsoft.com"}
|
||||
<i class="bi bi-microsoft"></i>
|
||||
{:else if oidc.provider == "apple.com"}
|
||||
<i class="bi bi-apple"></i>
|
||||
{/if}
|
||||
{$t("account.oidc-login", { provider: oidc.provider })}
|
||||
</Button>
|
||||
{/await}
|
||||
{/if}
|
||||
<Button type="button" on:click={handleForgottenPassword} outline color="dark">
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
<script lang="ts">
|
||||
import { Container, ListGroup, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
import { getOidcProvider } from "$lib/api/auth";
|
||||
import { isAuthUser } from "$lib/api/user";
|
||||
import { userSession } from "$lib/stores/usersession";
|
||||
import { t } from "$lib/translations";
|
||||
|
||||
|
|
@ -32,9 +34,7 @@
|
|||
import SessionsManager from "./SessionsManager.svelte";
|
||||
import UserSettingsForm from "./UserSettingsForm.svelte";
|
||||
|
||||
let is_auth_user_req = $userSession.id
|
||||
? fetch(`/api/users/${$userSession.id}/is_auth_user`)
|
||||
: false;
|
||||
let is_auth_user_req = $userSession.id ? isAuthUser($userSession) : false;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
|
@ -87,15 +87,13 @@
|
|||
<Spinner />
|
||||
</div>
|
||||
{:then res}
|
||||
{#if res && res.status === 204}
|
||||
{#if res}
|
||||
<ChangePasswordForm />
|
||||
{:else}
|
||||
{#await fetch("/auth/has_oidc") then res}
|
||||
{#await res.json() then oidc}
|
||||
<div class="alert alert-secondary">
|
||||
{$t("account.no-password-change", { provider: oidc.provider })}
|
||||
</div>
|
||||
{/await}
|
||||
{#await getOidcProvider() then oidc}
|
||||
<div class="alert alert-secondary">
|
||||
{$t("account.no-password-change", { provider: oidc.provider })}
|
||||
</div>
|
||||
{/await}
|
||||
{/if}
|
||||
{/await}
|
||||
|
|
@ -113,7 +111,7 @@
|
|||
</div>
|
||||
{:then res}
|
||||
<ListGroup>
|
||||
<DeleteAccountCard externalAuth={res && res.status !== 204} />
|
||||
<DeleteAccountCard externalAuth={!res} />
|
||||
</ListGroup>
|
||||
{/await}
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, Input, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
timeout: 5000,
|
||||
color: "success",
|
||||
});
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
},
|
||||
(error) => {
|
||||
formSent = false;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
message: $t("account.delete.success"),
|
||||
type: "primary",
|
||||
});
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
function deletionError(err: Error): void {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
is_closing_sessions = true;
|
||||
await deleteSessions();
|
||||
is_closing_sessions = false;
|
||||
goto("/login");
|
||||
navigate("/login");
|
||||
}
|
||||
|
||||
let newSessionModalOpen = $state(false);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
|
||||
import { Button, ButtonGroup, Icon, Input, Spinner } from "@sveltestrap/sveltestrap";
|
||||
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
color: "success",
|
||||
});
|
||||
|
||||
goto("/");
|
||||
navigate("/");
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { navigate } from "$lib/stores/config";
|
||||
import { page } from "$app/state";
|
||||
import { untrack } from "svelte";
|
||||
|
||||
|
|
@ -137,7 +137,7 @@
|
|||
if (form.domain === domain) {
|
||||
resolve(form);
|
||||
} else {
|
||||
goto("/resolver/" + encodeURIComponent(form.domain), {
|
||||
navigate("/resolver/" + encodeURIComponent(form.domain), {
|
||||
state: { form, showDNSSEC },
|
||||
noScroll: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ import { build, files, version } from '$service-worker';
|
|||
// Create a unique cache name for this deployment
|
||||
const CACHE = `cache-${version}`;
|
||||
|
||||
// Derive base path from SW scope (e.g. "https://example.com/subpath/" → "/subpath")
|
||||
const BASE = new URL(self.registration.scope).pathname.replace(/\/$/, '');
|
||||
|
||||
const ASSETS = [
|
||||
...build, // the app itself
|
||||
...files // everything in `static`
|
||||
|
|
@ -50,9 +53,12 @@ self.addEventListener('fetch', (event) => {
|
|||
async function respond() {
|
||||
const url = new URL(event.request.url);
|
||||
const cache = await caches.open(CACHE);
|
||||
const pathname = BASE && url.pathname.startsWith(BASE)
|
||||
? url.pathname.slice(BASE.length)
|
||||
: url.pathname;
|
||||
|
||||
// `build`/`files` can always be served from the cache
|
||||
if (ASSETS.includes(url.pathname)) {
|
||||
if (ASSETS.includes(pathname)) {
|
||||
// Strip query string for asset cache lookup (assets are keyed by pathname only)
|
||||
const cacheKey = url.search.length
|
||||
? new Request(url.origin + url.pathname)
|
||||
|
|
@ -66,8 +72,8 @@ self.addEventListener('fetch', (event) => {
|
|||
}
|
||||
|
||||
if (
|
||||
url.pathname.startsWith("/api/providers/_specs") ||
|
||||
url.pathname.startsWith("/api/service_specs/")
|
||||
pathname.startsWith("/api/providers/_specs") ||
|
||||
pathname.startsWith("/api/service_specs/")
|
||||
) {
|
||||
// cache first
|
||||
const responseFromCache = await caches.match(event.request);
|
||||
|
|
@ -88,7 +94,7 @@ self.addEventListener('fetch', (event) => {
|
|||
try {
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (response.status === 200 && !url.pathname.startsWith("/api/auth")) {
|
||||
if (response.status === 200 && !pathname.startsWith("/api/auth")) {
|
||||
await cache.put(event.request, response.clone());
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue