Skip to content

Commit

Permalink
feat: add optional browser history routing
Browse files Browse the repository at this point in the history
Add new build-time-env `ROUTER_TYPE` defaults to 'hash' with optional
'history' value to use the browsers history api for routing.
Also refactor Dockerfile to improve performance on subsequent builds.
  • Loading branch information
Patrick-Remy committed Feb 11, 2025
1 parent 7f63fd6 commit deb24c1
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 15 deletions.
27 changes: 23 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
# Yopass Server build stage
FROM golang:bookworm AS app
RUN mkdir -p /yopass

WORKDIR /yopass
COPY . .
COPY go.mod go.sum LICENSE /yopass/
COPY cmd /yopass/cmd
COPY pkg /yopass/pkg

RUN go build ./cmd/yopass && go build ./cmd/yopass-server


# Website build stage
FROM node:22 AS website
COPY website /website
WORKDIR /website
RUN yarn install --network-timeout 600000 && yarn build

# Install dependencies
COPY website/package.json website/yarn.lock /website/
RUN yarn install --network-timeout 600000

# Build and bundle src
COPY website/tsconfig.json website/vite.config.ts website/index.html /website/
COPY website/src /website/src
COPY website/public /website/public

ARG PUBLIC_URL
ARG ROUTER_TYPE
RUN PUBLIC_URL="${PUBLIC_URL}" ROUTER_TYPE="${ROUTER_TYPE}" yarn build


# Final minimal image including only built resources
FROM gcr.io/distroless/base
COPY --from=app /yopass/yopass /yopass/yopass-server /
COPY --from=website /website/build /public
Expand Down
13 changes: 13 additions & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ func (y *Server) optionsCreateSecret(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", strings.Join([]string{http.MethodPost}, ","))
}

// getSpaHtml serve built spa file for additional virtual routes used in spa
func getSpaHtml(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "public/index.html")
}

// HTTPHandler containing all routes
func (y *Server) HTTPHandler() http.Handler {
mx := mux.NewRouter()
Expand All @@ -166,7 +171,15 @@ func (y *Server) HTTPHandler() http.Handler {
mx.HandleFunc("/file/"+keyParameter, y.deleteSecret).Methods(http.MethodDelete)
mx.HandleFunc("/file/"+keyParameter, y.optionsSecret).Methods(http.MethodOptions)

// Virtual routes used by spa should serve index.html, for ROUTER_TYPE=history
mx.HandleFunc("/upload", getSpaHtml).Methods(http.MethodGet)
mx.HandleFunc("/s/"+keyParameter, getSpaHtml).Methods(http.MethodGet)
mx.HandleFunc("/s/"+keyParameter+"/{secret}", getSpaHtml).Methods(http.MethodGet)
mx.HandleFunc("/f/"+keyParameter, getSpaHtml).Methods(http.MethodGet)
mx.HandleFunc("/f/"+keyParameter+"/{secret}", getSpaHtml).Methods(http.MethodGet)

mx.PathPrefix("/").Handler(http.FileServer(http.Dir("public")))

return handlers.CustomLoggingHandler(nil, SecurityHeadersHandler(mx), httpLogFormatter(y.logger))
}

Expand Down
21 changes: 19 additions & 2 deletions website/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ import { Features } from './shared/Features';
import { Attribution } from './shared/Attribution';
import { theme } from './theme';
import { HashRouter } from 'react-router-dom';
import { BrowserRouter } from 'react-router-dom';

const Router = ({ children }: React.PropsWithChildren) => {
if (process.env.ROUTER_TYPE === 'history') {
return (
<BrowserRouter>
{children}
</BrowserRouter>
)
}

return (
<HashRouter>
{children}
</HashRouter>
)
}

const App = () => {
// TODO: Removed in future version.
Expand All @@ -21,14 +38,14 @@ const App = () => {
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<HashRouter>
<Router>
<Header />
<Container maxWidth={'lg'}>
<Routing />
<Features />
<Attribution />
</Container>
</HashRouter>
</Router>
</ThemeProvider>
</StyledEngineProvider>
);
Expand Down
5 changes: 2 additions & 3 deletions website/src/displaySecret/Result.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Box,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useAbsoluteRouteBase } from '../utils/useBase';

type ResultProps = {
readonly uuid: string;
Expand All @@ -21,9 +22,7 @@ type ResultProps = {
};

const Result = ({ uuid, password, prefix, customPassword }: ResultProps) => {
const base =
(process.env.PUBLIC_URL ||
`${window.location.protocol}//${window.location.host}`) + `/#/${prefix}`;
const base = useAbsoluteRouteBase() + `/${prefix}`;
const short = `${base}/${uuid}`;
const full = `${short}/${password}`;
const { t } = useTranslation();
Expand Down
3 changes: 2 additions & 1 deletion website/src/i18n.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { useBaseUrl } from './utils/useBase';

i18n
.use(initReactI18next)
.use(Backend)
.use(LanguageDetector)
.init({
backend: {
loadPath: process.env.PUBLIC_URL + '/locales/{{lng}}.json',
loadPath: useBaseUrl() + '/locales/{{lng}}.json',
},

fallbackLng: process.env.REACT_APP_FALLBACK_LANGUAGE || 'en',
Expand Down
9 changes: 5 additions & 4 deletions website/src/shared/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AppBar, Toolbar, Typography, Button, Box, Link } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useAbsoluteRouteBase, useBaseUrl } from '../utils/useBase';

export const Header = () => {
const { t } = useTranslation();
const location = useLocation();
const isOnUploadPage = location.pathname.includes('upload');
const base = process.env.PUBLIC_URL || '';
const home = base + '/#/';
const upload = base + '/#/upload';
const base = useAbsoluteRouteBase();
const home = base + '/';
const upload = base + '/upload';
return (
<AppBar position="static" color="transparent" sx={{ marginBottom: 4 }}>
<Toolbar>
Expand All @@ -25,7 +26,7 @@ export const Header = () => {
component="img"
height="40"
alt=""
src="yopass.svg"
src={useBaseUrl() + '/yopass.svg'}
/>
</Typography>
</Link>
Expand Down
12 changes: 12 additions & 0 deletions website/src/utils/useBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const useBaseUrl = function (): string {
return (process.env.PUBLIC_URL ||
`${window.location.protocol}//${window.location.host}`);
}

export const useAbsoluteRouteBase = function (): string {
return useBaseUrl() + useRelativeRouteBase();
}

export const useRelativeRouteBase = function (): string {
return process.env.ROUTER_TYPE === 'hash' ? '/#' : '';
}
7 changes: 6 additions & 1 deletion website/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import react from '@vitejs/plugin-react';
import { defineConfig, UserConfigExport } from 'vite';

export default defineConfig(() => {
const PUBLIC_URL = process.env.PUBLIC_URL || '';
// Ensure PUBLIC_URL is not an empty string, otherwise vite base url is
// relative to currently accessed path instead of '/'
const PUBLIC_URL = process.env.PUBLIC_URL || undefined;
const ROUTER_TYPE = process.env.ROUTER_TYPE === 'history' ? 'history' : 'hash';

const config: UserConfigExport = {
plugins: [react()],
Expand All @@ -24,6 +27,8 @@ export default defineConfig(() => {
CI: process.env.CI,
NODE_ENV: process.env.NODE_ENV,
PUBLIC_URL,
ROUTER_TYPE,
ROUTER_API: process.env.ROUTER_API === 'history' ,
REACT_APP_BACKEND_URL: process.env.REACT_APP_BACKEND_URL,
REACT_APP_FALLBACK_LANGUAGE: process.env.REACT_APP_FALLBACK_LANGUAGE,
START_SERVER_AND_TEST_INSECURE:
Expand Down

0 comments on commit deb24c1

Please sign in to comment.