mirror of
https://github.com/plankanban/planka.git
synced 2026-05-04 18:00:55 +03:00
feat: Support running under subpath (#1451)
This commit is contained in:
committed by
GitHub
parent
61a3ff55cc
commit
5e6195b252
@@ -11,7 +11,7 @@ server/test
|
||||
server/.tmp
|
||||
server/.venv
|
||||
|
||||
server/views/index.html
|
||||
server/views/index.ejs
|
||||
|
||||
server/data/*
|
||||
!server/data/.gitkeep
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
working-directory: ./client
|
||||
|
||||
- name: Build client
|
||||
run: DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
run: INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Include licenses into dist
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Include built client into dist
|
||||
run: |
|
||||
mv ../../client/dist/* public
|
||||
cp public/index.html views
|
||||
mv public/index.ejs views
|
||||
working-directory: ./server/dist
|
||||
|
||||
- name: Create release package
|
||||
|
||||
@@ -24,13 +24,13 @@ jobs:
|
||||
|
||||
- name: Build client
|
||||
run: |
|
||||
DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
mv dist build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Update Dockerfile to use prebuilt client
|
||||
run: |
|
||||
sed -i '/^FROM node:22 AS client/,/^ && DISABLE_ESLINT_PLUGIN=true npm run build$/c\
|
||||
sed -i '/^FROM node:22 AS client/,/^ && INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build$/c\
|
||||
FROM node:22 AS client\n\
|
||||
WORKDIR /app\n\
|
||||
COPY client/build /app/dist' Dockerfile
|
||||
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
|
||||
- name: Build client
|
||||
run: |
|
||||
DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
mv dist build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Update Dockerfile to use prebuilt client
|
||||
run: |
|
||||
sed -i '/^FROM node:22 AS client/,/^ && DISABLE_ESLINT_PLUGIN=true npm run build$/c\
|
||||
sed -i '/^FROM node:22 AS client/,/^ && INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build$/c\
|
||||
FROM node:22 AS client\n\
|
||||
WORKDIR /app\n\
|
||||
COPY client/build /app/dist' Dockerfile
|
||||
|
||||
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
run: |
|
||||
npm install
|
||||
cd client
|
||||
npm run build
|
||||
INDEX_FORMAT=ejs npm run build
|
||||
|
||||
- name: Set up and start server for testing
|
||||
env:
|
||||
|
||||
@@ -22,7 +22,7 @@ COPY client .
|
||||
|
||||
RUN npm install npm --global \
|
||||
&& npm install --omit=dev \
|
||||
&& DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
&& INDEX_FORMAT=ejs DISABLE_ESLINT_PLUGIN=true npm run build
|
||||
|
||||
# Stage 3: Final image
|
||||
FROM node:22-alpine
|
||||
@@ -41,12 +41,12 @@ COPY --from=server --chown=node:node /app/node_modules node_modules
|
||||
COPY --from=server --chown=node:node /app/dist .
|
||||
|
||||
COPY --from=client --chown=node:node /app/dist public
|
||||
COPY --from=client --chown=node:node /app/dist/index.html views
|
||||
|
||||
RUN python3 -m venv .venv \
|
||||
&& .venv/bin/pip3 install --upgrade pip \
|
||||
&& .venv/bin/pip3 install -r requirements.txt --no-cache-dir \
|
||||
&& mv .env.sample .env \
|
||||
&& mv public/index.ejs views \
|
||||
&& npm config set update-notifier false
|
||||
|
||||
VOLUME /app/data
|
||||
|
||||
@@ -18,7 +18,7 @@ const http = {};
|
||||
return result;
|
||||
}, new FormData());
|
||||
|
||||
return fetch(`${Config.SERVER_BASE_URL}/api${url}`, {
|
||||
return fetch(`${Config.BASE_PATH}/api${url}`, {
|
||||
method,
|
||||
headers,
|
||||
body: formData,
|
||||
|
||||
@@ -10,7 +10,7 @@ import Config from '../constants/Config';
|
||||
|
||||
const io = sailsIOClient(socketIOClient);
|
||||
|
||||
io.sails.url = Config.SERVER_BASE_URL;
|
||||
io.sails.path = `${Config.BASE_PATH}/socket.io`;
|
||||
io.sails.autoConnect = false;
|
||||
io.sails.reconnection = true;
|
||||
io.sails.useCORSRouteToGetCookie = false;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSelector } from 'react-redux';
|
||||
import history from '../../../history';
|
||||
import selectors from '../../../selectors';
|
||||
import matchPaths from '../../../utils/match-paths';
|
||||
import Config from '../../../constants/Config';
|
||||
import Paths from '../../../constants/Paths';
|
||||
|
||||
const Link = React.memo(({ href, content, stopPropagation, ...props }) => {
|
||||
@@ -23,7 +24,8 @@ const Link = React.memo(({ href, content, stopPropagation, ...props }) => {
|
||||
}
|
||||
}, [href]);
|
||||
|
||||
const isSameSite = !!url && url.origin === window.location.origin;
|
||||
const isSameSite =
|
||||
!!url && url.origin === window.location.origin && url.pathname.startsWith(Config.BASE_PATH);
|
||||
|
||||
const cardsPathMatch = useMemo(() => {
|
||||
if (!isSameSite) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import history from '../../history';
|
||||
import Config from '../../constants/Config';
|
||||
|
||||
const SAME_SITE_CLASS = 'same-site';
|
||||
|
||||
@@ -30,7 +31,9 @@ function process(token, nextToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSameSite = url.origin === window.location.origin;
|
||||
const isSameSite =
|
||||
url.origin === window.location.origin && url.pathname.startsWith(Config.BASE_PATH);
|
||||
|
||||
const trimOrigin = isSameSite && nextToken.type === 'text' && nextToken.content === href;
|
||||
|
||||
if (isSameSite) {
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const SERVER_BASE_URL =
|
||||
import.meta.env.VITE_SERVER_BASE_URL || (import.meta.env.DEV ? 'http://localhost:1337' : '');
|
||||
const BASE_PATH = window.BASE_PATH || '';
|
||||
|
||||
const ACCESS_TOKEN_KEY = 'accessToken';
|
||||
const ACCESS_TOKEN_VERSION_KEY = 'accessTokenVersion';
|
||||
@@ -20,7 +19,7 @@ const MAX_SIZE_TO_DISPLAY_CONTENT = 256 * 1024;
|
||||
const IS_MAC = navigator.platform.startsWith('Mac');
|
||||
|
||||
export default {
|
||||
SERVER_BASE_URL,
|
||||
BASE_PATH,
|
||||
ACCESS_TOKEN_KEY,
|
||||
ACCESS_TOKEN_VERSION_KEY,
|
||||
ACCESS_TOKEN_VERSION,
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const ROOT = '/';
|
||||
const LOGIN = '/login';
|
||||
const OIDC_CALLBACK = '/oidc-callback';
|
||||
const PROJECTS = '/projects/:id';
|
||||
const BOARDS = '/boards/:id';
|
||||
const CARDS = '/cards/:id';
|
||||
import Config from './Config';
|
||||
|
||||
const ROOT = `${Config.BASE_PATH}/`;
|
||||
const LOGIN = `${Config.BASE_PATH}/login`;
|
||||
const OIDC_CALLBACK = `${Config.BASE_PATH}/oidc-callback`;
|
||||
const PROJECTS = `${Config.BASE_PATH}/projects/:id`;
|
||||
const BOARDS = `${Config.BASE_PATH}/boards/:id`;
|
||||
const CARDS = `${Config.BASE_PATH}/cards/:id`;
|
||||
|
||||
export default {
|
||||
ROOT,
|
||||
|
||||
@@ -8,24 +8,28 @@ import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
import Config from '../constants/Config';
|
||||
|
||||
const PATH = Config.BASE_PATH || '/';
|
||||
|
||||
export const setAccessToken = (accessToken) => {
|
||||
const { exp } = jwtDecode(accessToken);
|
||||
const expires = new Date(exp * 1000);
|
||||
|
||||
Cookies.set(Config.ACCESS_TOKEN_KEY, accessToken, {
|
||||
expires,
|
||||
path: PATH,
|
||||
secure: window.location.protocol === 'https:',
|
||||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
Cookies.set(Config.ACCESS_TOKEN_VERSION_KEY, Config.ACCESS_TOKEN_VERSION, {
|
||||
expires,
|
||||
path: PATH,
|
||||
});
|
||||
};
|
||||
|
||||
export const removeAccessToken = () => {
|
||||
Cookies.remove(Config.ACCESS_TOKEN_KEY);
|
||||
Cookies.remove(Config.ACCESS_TOKEN_VERSION_KEY);
|
||||
Cookies.remove(Config.ACCESS_TOKEN_KEY, { path: PATH });
|
||||
Cookies.remove(Config.ACCESS_TOKEN_VERSION_KEY, { path: PATH });
|
||||
};
|
||||
|
||||
export const getAccessToken = () => {
|
||||
|
||||
@@ -17,6 +17,6 @@ ln -s ${CLIENT_PATH}/logo512.png ${SERVER_PUBLIC_PATH}/logo512.png && echo "Link
|
||||
ln -s ${CLIENT_PATH}/manifest.json ${SERVER_PUBLIC_PATH}/manifest.json && echo "Linked manifest.json successfully"
|
||||
ln -s ${CLIENT_PATH}/robots.txt ${SERVER_PUBLIC_PATH}/robots.txt && echo "Linked robots.txt successfully"
|
||||
ln -s ${CLIENT_PATH}/assets ${SERVER_PUBLIC_PATH}/assets && echo "Linked assets folder successfully"
|
||||
ln -s ${CLIENT_PATH}/index.html ${SERVER_VIEWS_PATH}/index.html && echo "Linked index.html successfully"
|
||||
ln -s ${CLIENT_PATH}/index.ejs ${SERVER_VIEWS_PATH}/index.ejs && echo "Linked index.ejs successfully"
|
||||
|
||||
echo "Setup symbolic links completed successfully."
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { defineConfig } from 'vite';
|
||||
import commonjs from 'vite-plugin-commonjs';
|
||||
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import react from '@vitejs/plugin-react';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import browserslistToEsbuild from 'browserslist-to-esbuild';
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const createEjsTemplate = () => ({
|
||||
name: 'create-ejs-template',
|
||||
closeBundle() {
|
||||
if (process.env.INDEX_FORMAT !== 'ejs') return;
|
||||
|
||||
const distPath = path.resolve(__dirname, 'dist');
|
||||
const htmlPath = path.join(distPath, 'index.html');
|
||||
|
||||
if (!fs.existsSync(htmlPath)) return;
|
||||
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
|
||||
const ejs = html
|
||||
.replace(/(href|src)="\.\/([^"]+)"/g, '$1="<%- basePath %>/$2"')
|
||||
.replace('</head>', " <script>window.BASE_PATH = '<%- basePath %>';</script>\n </head>");
|
||||
|
||||
fs.writeFileSync(path.join(distPath, 'index.ejs'), ejs);
|
||||
fs.unlinkSync(htmlPath);
|
||||
},
|
||||
});
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [
|
||||
commonjs(),
|
||||
nodePolyfills({
|
||||
@@ -15,6 +44,7 @@ export default defineConfig({
|
||||
}),
|
||||
react(),
|
||||
svgr(),
|
||||
createEjsTemplate(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -24,6 +54,10 @@ export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': 'http://localhost:1337',
|
||||
'/socket.io': { target: 'http://localhost:1337', ws: true },
|
||||
},
|
||||
},
|
||||
build: {
|
||||
target: browserslistToEsbuild(['>0.2%', 'not dead', 'not op_mini all']),
|
||||
|
||||
@@ -20,7 +20,7 @@ test
|
||||
.tmp
|
||||
.venv
|
||||
|
||||
views/index.html
|
||||
views/index.ejs
|
||||
|
||||
data/*
|
||||
|
||||
|
||||
2
server/.gitignore
vendored
2
server/.gitignore
vendored
@@ -134,7 +134,7 @@ swagger.json
|
||||
dist
|
||||
logs
|
||||
|
||||
views/index.html
|
||||
views/index.ejs
|
||||
|
||||
data/*
|
||||
!data/.gitkeep
|
||||
|
||||
19
server/api/controllers/index.js
Normal file
19
server/api/controllers/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/*!
|
||||
* Copyright (c) 2025 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
exits: {
|
||||
success: {
|
||||
responseType: 'view',
|
||||
viewTemplatePath: 'index',
|
||||
},
|
||||
},
|
||||
|
||||
fn() {
|
||||
return {
|
||||
basePath: sails.config.custom.baseUrlPath,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -15,7 +15,7 @@ module.exports = {
|
||||
|
||||
fn(inputs) {
|
||||
inputs.response.clearCookie('httpOnlyToken', {
|
||||
path: sails.config.custom.baseUrlPath,
|
||||
path: sails.config.custom.baseUrlPath || '/',
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports = {
|
||||
fn(inputs) {
|
||||
inputs.response.cookie('httpOnlyToken', inputs.value, {
|
||||
expires: new Date(inputs.accessTokenPayload.exp * 1000),
|
||||
path: sails.config.custom.baseUrlPath,
|
||||
path: sails.config.custom.baseUrlPath || '/',
|
||||
secure: sails.config.custom.baseUrlSecure,
|
||||
httpOnly: true,
|
||||
sameSite: 'strict',
|
||||
|
||||
@@ -41,7 +41,7 @@ module.exports.custom = {
|
||||
version,
|
||||
|
||||
baseUrl,
|
||||
baseUrlPath: parsedBasedUrl.pathname,
|
||||
baseUrlPath: parsedBasedUrl.pathname.replace(/\/$/, ''), // Remove trailing slash
|
||||
baseUrlSecure: parsedBasedUrl.protocol === 'https:',
|
||||
|
||||
maxUploadFileSize: envToBytes(process.env.MAX_UPLOAD_FILE_SIZE),
|
||||
|
||||
@@ -44,6 +44,7 @@ module.exports.policies = {
|
||||
|
||||
'_internal/update-config': ['is-authenticated', 'is-internal'],
|
||||
|
||||
index: true,
|
||||
'swagger/show': true,
|
||||
'bootstrap/show': true,
|
||||
'terms/show': true,
|
||||
|
||||
@@ -266,7 +266,7 @@ module.exports.routes = {
|
||||
},
|
||||
|
||||
'GET /*': {
|
||||
view: 'index',
|
||||
action: 'index',
|
||||
skipAssets: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ module.exports.views = {
|
||||
*
|
||||
*/
|
||||
|
||||
extension: 'html',
|
||||
// extension: 'ejs',
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user