Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
**/.classpath
**/.dockerignore
**/.env
!frontend/.env
**/.git
**/.gitignore
**/.project
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/handlers/v1/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func NewControllerV1(svc *services.AllServices, repos *repo.AllRepos, bus *event

func (ctrl *V1Controller) initOIDCProvider() {
if ctrl.config.OIDC.Enabled {
oidcProvider, err := providers.NewOIDCProvider(ctrl.svc.User, &ctrl.config.OIDC, &ctrl.config.Options, ctrl.cookieSecure)
oidcProvider, err := providers.NewOIDCProvider(ctrl.svc.User, &ctrl.config.OIDC, &ctrl.config.Options, &ctrl.config.Web, ctrl.cookieSecure)
if err != nil {
log.Err(err).Msg("failed to initialize OIDC provider at startup")
} else {
Expand Down
4 changes: 2 additions & 2 deletions backend/app/api/handlers/v1/v1_ctrl_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,13 +337,13 @@ func (ctrl *V1Controller) HandleOIDCCallback() errchain.HandlerFunc {
newToken, err := ctrl.oidcProvider.HandleCallback(w, r)
if err != nil {
log.Err(err).Msg("OIDC callback failed")
http.Redirect(w, r, "/?oidc_error=oidc_auth_failed", http.StatusFound)
http.Redirect(w, r, ctrl.config.Web.AppBase+"?oidc_error=oidc_auth_failed", http.StatusFound)
return nil
}

// Set cookies and redirect to home
ctrl.setCookies(w, noPort(r.Host), newToken.Raw, newToken.ExpiresAt, true, newToken.AttachmentToken)
http.Redirect(w, r, "/home", http.StatusFound)
http.Redirect(w, r, ctrl.config.Web.AppBase+"home", http.StatusFound)
return nil
}
}
18 changes: 10 additions & 8 deletions backend/app/api/providers/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type OIDCProvider struct {
service *services.UserService
config *config.OIDCConf
options *config.Options
webConfig *config.WebConfig
cookieSecure bool
provider *oidc.Provider
verifier *oidc.IDTokenVerifier
Expand All @@ -39,7 +40,7 @@ type OIDCClaims struct {
EmailVerified *bool
}

func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, options *config.Options, cookieSecure bool) (*OIDCProvider, error) {
func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, options *config.Options, webConfig *config.WebConfig, cookieSecure bool) (*OIDCProvider, error) {
if !config.Enabled {
return nil, fmt.Errorf("OIDC is not enabled")
}
Expand Down Expand Up @@ -106,6 +107,7 @@ func NewOIDCProvider(service *services.UserService, config *config.OIDCConf, opt
service: service,
config: config,
options: options,
webConfig: webConfig,
cookieSecure: cookieSecure,
provider: provider,
verifier: verifier,
Expand Down Expand Up @@ -416,7 +418,7 @@ func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request)
Domain: domain,
Secure: p.isSecure(r),
HttpOnly: true,
Path: "/",
Path: u.Path,
SameSite: http.SameSiteLaxMode,
})

Expand All @@ -428,7 +430,7 @@ func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request)
Domain: domain,
Secure: p.isSecure(r),
HttpOnly: true,
Path: "/",
Path: u.Path,
SameSite: http.SameSiteLaxMode,
})

Expand All @@ -440,7 +442,7 @@ func (p *OIDCProvider) initiateOIDCFlow(w http.ResponseWriter, r *http.Request)
Domain: domain,
Secure: p.isSecure(r),
HttpOnly: true,
Path: "/",
Path: u.Path,
SameSite: http.SameSiteLaxMode,
})

Expand Down Expand Up @@ -470,7 +472,7 @@ func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (s
MaxAge: -1,
Secure: p.isSecure(r),
HttpOnly: true,
Path: "/",
Path: u.Path,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{
Expand All @@ -481,7 +483,7 @@ func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (s
MaxAge: -1,
Secure: p.isSecure(r),
HttpOnly: true,
Path: "/",
Path: u.Path,
SameSite: http.SameSiteLaxMode,
})
http.SetCookie(w, &http.Cookie{
Expand All @@ -492,7 +494,7 @@ func (p *OIDCProvider) handleCallback(w http.ResponseWriter, r *http.Request) (s
MaxAge: -1,
Secure: p.isSecure(r),
HttpOnly: true,
Path: "/",
Path: u.Path,
SameSite: http.SameSiteLaxMode,
})
}
Expand Down Expand Up @@ -600,7 +602,7 @@ func (p *OIDCProvider) getBaseURL(r *http.Request) string {
}
}

return scheme + "://" + host
return scheme + "://" + host + p.webConfig.AppBase
}

func (p *OIDCProvider) isSecure(r *http.Request) bool {
Expand Down
2 changes: 1 addition & 1 deletion backend/app/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (a *app) mountRoutes(r *chi.Mux, chain *errchain.ErrChain, repos *repo.AllR
v1.WithURL(fmt.Sprintf("%s:%s", a.conf.Web.Host, a.conf.Web.Port)),
)

r.Route(prefix+"/v1", func(r chi.Router) {
r.Route(path.Join(a.conf.Web.AppBase, prefix, "/v1"), func(r chi.Router) {
r.Get("/status", chain.ToHandlerFunc(v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
Version: version,
Commit: commit,
Expand Down
20 changes: 20 additions & 0 deletions backend/internal/sys/config/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"errors"
"fmt"
"os"
"strings"
"time"

"github.com/ardanlabs/conf/v3"
Expand Down Expand Up @@ -61,6 +62,7 @@
type WebConfig struct {
Port string `yaml:"port" conf:"default:7745"`
Host string `yaml:"host"`
AppBase string `yaml:"app_base" conf:"default:/"`
MaxUploadSize int64 `yaml:"max_file_upload" conf:"default:10"`
ReadTimeout time.Duration `yaml:"read_timeout" conf:"default:10s"`
WriteTimeout time.Duration `yaml:"write_timeout" conf:"default:10s"`
Expand Down Expand Up @@ -137,9 +139,27 @@
return &cfg, fmt.Errorf("parsing config: %w", err)
}

cfg.Web.AppBase = normalizePath(cfg.Web.AppBase)

return &cfg, nil
}

// normalizePath ensures a path is never empty and always has a leading and
// trailing slash. This prevents malformed URL concatenation when AppBase is
// used as a path prefix.
func normalizePath(p string) string {
if p == "" {
return "/"
}
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
if !strings.HasSuffix(p, "/") {
p = p + "/"

Check failure on line 158 in backend/internal/sys/config/conf.go

View workflow job for this annotation

GitHub Actions / Backend Server Tests / Go

assignOp: replace `p = p + "/"` with `p += "/"` (gocritic)
}
return p
}

// Print prints the configuration to stdout as a json indented string
// This is useful for debugging. If the marshaller errors out, it will panic.
func (c *Config) Print() {
Expand Down
1 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NUXT_APP_BASE_URL=/
4 changes: 2 additions & 2 deletions frontend/components/Collection/JoinModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
</span>
<div>
<Badge variant="outline"> AYQ4W4K5MT4CZOPB2PZRCZ4PTY </Badge>
<Badge variant="outline"> {{ `${domain}?token=AYQ4W4K5MT4CZOPB2PZRCZ4PTY` }} </Badge>
<Badge variant="outline"> {{ `${baseUrl}?token=AYQ4W4K5MT4CZOPB2PZRCZ4PTY` }} </Badge>
</div>
</div>

Expand Down Expand Up @@ -54,7 +54,7 @@
const api = useUserApi();
const collections = useCollections();

const domain = window.location.protocol + "//" + window.location.host;
const baseUrl = new URL(useAppBase(), `${window.location.protocol}//${window.location.host}`).toString();

const redirectTo = ref("");
// Holds invite code from dialog params until the watcher applies it on open
Expand Down
8 changes: 8 additions & 0 deletions frontend/composables/use-app-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Use the base URL of the app.
* @returns `config.app.baseURL` of Nuxt config
*/
export function useAppBase(): string {
const config = useRuntimeConfig();
return config.app.baseURL;
}
4 changes: 3 additions & 1 deletion frontend/composables/use-server-events.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useAppBase } from "./use-app-base";
import { useViewPreferences } from "./use-preferences";
import { watch } from "vue";

Expand Down Expand Up @@ -27,8 +28,9 @@ function connect(onmessage: (m: EventMessage) => void) {
const dev = import.meta.dev;

const host = dev ? window.location.host.replace("3000", "7745") : window.location.host;
const base = useAppBase();

let url = `${protocol}://${host}/api/v1/ws/events`;
let url = `${protocol}://${host}${base}api/v1/ws/events`;
if (currentTenantId) {
url += `?tenant=${currentTenantId}`;
}
Expand Down
7 changes: 5 additions & 2 deletions frontend/lib/api/base/urls.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const parts = {
base: "/",
host: "http://localhost.com",
prefix: "/api/v1",
};

export function overrideParts(host: string, prefix: string) {
export function overrideParts(host: string, prefix: string, base: string = "/") {
parts.base = base;
parts.host = host;
parts.prefix = prefix;
}
Expand All @@ -20,7 +22,8 @@ export type QueryValue = string | string[] | number | number[] | boolean | null
* relative URLs in production because the API and client bundle are served from the same server/host.
*/
export function route(rest: string, params: Record<string, QueryValue> = {}): string {
const url = new URL(parts.prefix + rest, parts.host);
const base = parts.base.replace(/\/$/, "");
const url = new URL(base + parts.prefix + rest, parts.host);

for (const [key, value] of Object.entries(params)) {
if (Array.isArray(value)) {
Expand Down
26 changes: 20 additions & 6 deletions frontend/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { defineNuxtConfig } from "nuxt/config";

/**
* Ensure base path has leading and trailing slash
*/
const normalizeBase = (value = "/") => {
const trimmed = value.trim();
if (!trimmed || trimmed === "/") {
return "/";
}
return `/${trimmed.replace(/^\/+|\/+$/g, "")}/`;
};

const baseURL = normalizeBase(process.env.NUXT_APP_BASE_URL);

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
ssr: false,
Expand Down Expand Up @@ -41,29 +54,30 @@ export default defineNuxtConfig({

nitro: {
devProxy: {
"/api": {
target: "http://localhost:7745/api",
[baseURL + "api"]: {
target: new URL(baseURL + "api", "http://localhost:7745"),
ws: true,
changeOrigin: true,
},
},
},

app: {
baseURL,
head: {
script: [{ src: "/set-theme.js" }],
script: [{ src: baseURL + "set-theme.js" }],
},
},

css: ["@/assets/css/main.css"],

pwa: {
workbox: {
navigateFallbackDenylist: [/^\/api/],
navigateFallbackDenylist: [RegExp(`^${baseURL}api`)],
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /^\/api/,
urlPattern: RegExp(`^${baseURL}api`),
handler: "NetworkFirst",
method: "GET",
options: {
Expand All @@ -88,7 +102,7 @@ export default defineNuxtConfig({
short_name: "Homebox",
description: "Home Inventory App",
theme_color: "#5b7f67",
start_url: "/home",
start_url: baseURL + "home",
icons: [
{
src: "pwa-192x192.png",
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/collection/index/invites.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
const { openDialog } = useDialog();
const confirm = useConfirm();
const localInvites = ref<Invitation[]>([]);
const baseUrl = `${window.location.protocol}//${window.location.host}`;
const baseUrl = new URL(useAppBase(), `${window.location.protocol}//${window.location.host}`).toString();
const loading = ref(true);
const invites = ref<Invitation[]>([]);
const error = ref<string | null>(null);
Expand Down
4 changes: 3 additions & 1 deletion frontend/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import FormPassword from "~/components/Form/Password.vue";
import FormCheckbox from "~/components/Form/Checkbox.vue";
import PasswordScore from "~/components/global/PasswordScore.vue";
import { useAppBase } from "~/composables/use-app-base";

const { t } = useI18n();

Expand Down Expand Up @@ -213,7 +214,8 @@
}

function loginWithOIDC() {
window.location.href = "/api/v1/users/login/oidc";
const base = useAppBase();
window.location.href = base + "api/v1/users/login/oidc";
}

const [registerForm, toggleLogin] = useToggle();
Expand Down
11 changes: 11 additions & 0 deletions frontend/plugins/api-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Injects the Nuxt runtime base URL into the API URL builder so that
* `lib/api/` remains a pure TS layer with no composable dependencies.
*/

import { overrideParts } from "~~/lib/api/base/urls";

export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
overrideParts("http://localhost.com", "/api/v1", config.app.baseURL);
Comment thread
mdrkrg marked this conversation as resolved.
});
Loading