mirror of
https://github.com/flameshikari/outline-ru.git
synced 2026-06-13 12:15:15 +03:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e026250893 | |||
| d2be66831d | |||
| 49472ac801 | |||
| a81bd3bd17 | |||
| f22d5952bb | |||
| 02bdd461b4 | |||
| d0abf84aa8 | |||
| b3bda3622c | |||
| 856cf4b0c9 | |||
| 0bc1c14a9c | |||
| 2c681f14f2 | |||
| 2914b54933 | |||
| 076879fd63 | |||
| fbe5d1adb1 |
+5
-2
@@ -1,10 +1,12 @@
|
||||
__mocks__
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
.github
|
||||
.circleci
|
||||
.DS_Store
|
||||
.env*
|
||||
.eslint*
|
||||
.oxlintrc*
|
||||
.log
|
||||
Makefile
|
||||
Procfile
|
||||
@@ -12,4 +14,5 @@ app.json
|
||||
crowdin.yml
|
||||
build
|
||||
docker-compose.yml
|
||||
node_modules
|
||||
node_modules
|
||||
.yarn
|
||||
@@ -1,11 +1,6 @@
|
||||
APP_PATH=/opt/outline
|
||||
SRC_PATH=./outline
|
||||
COMPOSE_PROFILES=dev
|
||||
|
||||
ADDRESS=localhost
|
||||
PORT_OUTLINE=10240
|
||||
PORT_OIDC=10241
|
||||
PORT_REDIS=10242
|
||||
PORT_POSTGRES=10243
|
||||
|
||||
COMMON=outline
|
||||
SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
|
||||
# PORT=10240
|
||||
# PORT_OIDC=10241
|
||||
# PORT_VITE=10242
|
||||
# SECRET=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef # openssl rand -hex 32
|
||||
|
||||
+17
-19
@@ -8,9 +8,8 @@ on:
|
||||
paths:
|
||||
- .github/workflows/**
|
||||
- outline/**
|
||||
- tools/translation.json
|
||||
- tools/patches/**
|
||||
- Dockerfile.prod
|
||||
- translation/ru.json
|
||||
- Dockerfile
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -37,21 +36,21 @@ jobs:
|
||||
- arm64
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub CR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -65,7 +64,7 @@ jobs:
|
||||
echo "version=$version" | tee -a $GITHUB_OUTPUT
|
||||
|
||||
- name: Set Metadata
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
id: metadata
|
||||
with:
|
||||
images: |
|
||||
@@ -73,13 +72,12 @@ jobs:
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
id: build
|
||||
with:
|
||||
cache-from: type=gha,scope=build-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
file: Dockerfile.prod
|
||||
build-args: |
|
||||
APP_PATH=/opt/outline
|
||||
SRC_PATH=./outline
|
||||
@@ -93,7 +91,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload Digests
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: digests-linux-${{ matrix.arch }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -105,36 +103,36 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build
|
||||
permissions:
|
||||
contents: read
|
||||
contents: write
|
||||
packages: write
|
||||
env:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
steps:
|
||||
- name: Download Digests
|
||||
uses: actions/download-artifact@v6
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub CR
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Set Metadata
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
@@ -153,10 +151,10 @@ jobs:
|
||||
$(printf 'ghcr.io/${{ github.repository }}@sha256:%s ' *)
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
with:
|
||||
name: ${{ env.version }}
|
||||
tag_name: ${{ env.version }}
|
||||
body: "[Изменения в ${{ env.version }}](https://github.com/outline/outline/releases/tag/v${{ env.version }})"
|
||||
token: ${{ secrets.TOKEN }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
+39
-28
@@ -1,35 +1,46 @@
|
||||
FROM node:22.21.0 AS base
|
||||
ARG APP_PATH=/opt/outline
|
||||
ARG SRC_PATH=./outline
|
||||
|
||||
FROM node:24.16.0 AS build
|
||||
ARG CDN_URL
|
||||
ARG APP_PATH
|
||||
ARG SRC_PATH
|
||||
WORKDIR $APP_PATH
|
||||
|
||||
FROM base AS deps
|
||||
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
|
||||
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
|
||||
yarn cache clean
|
||||
|
||||
FROM base AS build
|
||||
RUN apt-get update && \
|
||||
apt-get install -y patch cmake && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ${SRC_PATH}/.yarnrc.yml ./
|
||||
COPY ${SRC_PATH}/patches ./patches
|
||||
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
|
||||
ENV NODE_OPTIONS="--max-old-space-size=24000"
|
||||
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
|
||||
ENV NODE_OPTIONS='--max-old-space-size=24000'
|
||||
RUN corepack enable && \
|
||||
yarn install --immutable --network-timeout 1000000 && \
|
||||
yarn cache clean
|
||||
COPY ${SRC_PATH} .
|
||||
COPY --from=deps $APP_PATH/node_modules ./node_modules
|
||||
COPY ./patches/* .
|
||||
RUN for patch in $(ls *.patch); do patch -p1 < $patch; done
|
||||
RUN cat <<EOF > /entrypoint.sh
|
||||
npx yarn concurrently -n "dev,i18n" \
|
||||
"yarn dev:watch" \
|
||||
"yarn nodemon \
|
||||
--watch './shared/i18n/locales/ru_RU' \
|
||||
--exec 'yarn build:i18n'"
|
||||
EOF
|
||||
ARG CDN_URL
|
||||
ARG DATA_PATH=/var/lib/outline/data
|
||||
COPY ./patches/lang.patch .
|
||||
RUN patch -p1 < lang.patch
|
||||
COPY ./translation/ru.json ./shared/i18n/locales/ru_RU/translation.json
|
||||
RUN yarn build && \
|
||||
yarn workspaces focus --production
|
||||
|
||||
FROM node:24.16.0-slim AS release
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ENV DATA_PATH=/var/lib/outline/data
|
||||
ENV USER=nodejs
|
||||
RUN addgroup --gid 1001 ${USER} && \
|
||||
adduser --uid 1001 --ingroup ${USER} ${USER} && \
|
||||
mkdir -p ${DATA_PATH} && \
|
||||
chown -R ${USER}:${USER} ${DATA_PATH}/..
|
||||
ARG APP_PATH
|
||||
WORKDIR $APP_PATH
|
||||
COPY --chown=${USER}:${USER} --from=build $APP_PATH/node_modules ./node_modules
|
||||
COPY --chown=${USER}:${USER} --from=build $APP_PATH/build ./build
|
||||
COPY --chown=${USER}:${USER} --from=build $APP_PATH/server ./server
|
||||
COPY --chown=${USER}:${USER} --from=build $APP_PATH/public ./public
|
||||
COPY --chown=${USER}:${USER} --from=build $APP_PATH/.sequelizerc .
|
||||
COPY --chown=${USER}:${USER} --from=build $APP_PATH/package.json .
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
USER ${USER}
|
||||
EXPOSE ${PORT}
|
||||
VOLUME ${DATA_PATH}
|
||||
STOPSIGNAL SIGKILL
|
||||
ENTRYPOINT ["bash", "/entrypoint.sh"]
|
||||
HEALTHCHECK --interval=1m CMD curl -fs localhost:${PORT}/_health | grep -q OK || exit 1
|
||||
CMD ["node", "build/server/index.js"]
|
||||
@@ -0,0 +1,24 @@
|
||||
ARG APP_PATH=/opt/outline
|
||||
ARG SRC_PATH=./outline
|
||||
|
||||
FROM node:24.15.0
|
||||
ARG APP_PATH
|
||||
ARG SRC_PATH
|
||||
ARG CDN_URL
|
||||
WORKDIR $APP_PATH
|
||||
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ${SRC_PATH}/.yarnrc.yml ./
|
||||
COPY ${SRC_PATH}/patches ./patches
|
||||
ENV NODE_OPTIONS='--max-old-space-size=24000'
|
||||
RUN corepack enable && \
|
||||
yarn install --immutable --network-timeout 1000000 && \
|
||||
yarn cache clean
|
||||
COPY ${SRC_PATH} .
|
||||
COPY ./patches/* .
|
||||
RUN for patch in $(ls *.patch); do patch -p1 < $patch; done
|
||||
RUN cat << EOF > /entrypoint.sh
|
||||
yarn dev:watch
|
||||
EOF
|
||||
ENV DATA_PATH=/var/lib/outline/data
|
||||
VOLUME ${DATA_PATH}
|
||||
STOPSIGNAL SIGKILL
|
||||
ENTRYPOINT ["bash", "/entrypoint.sh"]
|
||||
@@ -1,51 +0,0 @@
|
||||
FROM node:22.21.0 AS base
|
||||
ARG APP_PATH
|
||||
ARG SRC_PATH
|
||||
WORKDIR $APP_PATH
|
||||
|
||||
FROM base AS deps
|
||||
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
|
||||
RUN yarn install --production=true --frozen-lockfile --network-timeout 1000000 && \
|
||||
yarn cache clean
|
||||
|
||||
FROM base AS build
|
||||
RUN apt-get update && \
|
||||
apt-get install -y patch cmake && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
COPY ${SRC_PATH}/patches ./patches
|
||||
COPY ${SRC_PATH}/package.json ${SRC_PATH}/yarn.lock ./
|
||||
ENV NODE_OPTIONS="--max-old-space-size=24000"
|
||||
RUN yarn install --no-optional --frozen-lockfile --network-timeout 1000000 && \
|
||||
yarn cache clean
|
||||
COPY ${SRC_PATH} .
|
||||
COPY ./patches/lang.patch .
|
||||
RUN patch -p1 < lang.patch
|
||||
COPY ./translation/ru.json ./shared/i18n/locales/ru_RU/translation.json
|
||||
ARG CDN_URL
|
||||
RUN yarn build && rm -rf node_modules
|
||||
|
||||
FROM node:22.21.0-slim AS release
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ARG DATA_PATH=/var/lib/outline/data
|
||||
ARG USER=nodejs
|
||||
ARG APP_PATH
|
||||
WORKDIR $APP_PATH
|
||||
RUN addgroup --gid 1001 ${USER} && \
|
||||
adduser --uid 1001 --ingroup ${USER} ${USER} && \
|
||||
mkdir -p ${DATA_PATH} && \
|
||||
chown -R ${USER}:${USER} ${DATA_PATH}/..
|
||||
COPY --chown=${USER} --from=deps $APP_PATH/node_modules ./node_modules
|
||||
COPY --chown=${USER} --from=build $APP_PATH/build ./build
|
||||
COPY --chown=${USER} --from=build $APP_PATH/server ./server
|
||||
COPY --chown=${USER} --from=build $APP_PATH/public ./public
|
||||
COPY --chown=${USER} --from=build $APP_PATH/.sequelizerc .
|
||||
COPY --chown=${USER} --from=build $APP_PATH/package.json .
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
USER ${USER}
|
||||
EXPOSE ${PORT}
|
||||
VOLUME ${DATA_PATH}
|
||||
HEALTHCHECK --interval=1m CMD curl -fs localhost:${PORT}/_health | grep -q OK || exit 1
|
||||
CMD ["yarn", "start"]
|
||||
@@ -20,8 +20,8 @@
|
||||
```yaml
|
||||
services:
|
||||
outline:
|
||||
image: flameshikari/outline-ru:1.1.0
|
||||
# image: ghcr.io/flameshikari/outline-ru:1.1.0
|
||||
image: flameshikari/outline-ru:1.8.1
|
||||
# image: ghcr.io/flameshikari/outline-ru:1.8.1
|
||||
env_file: ./docker.env
|
||||
expose:
|
||||
- 3000
|
||||
@@ -93,7 +93,7 @@ services:
|
||||
|
||||
2. Пулл изменений в подмодуле и переключение на последний доступный тег:
|
||||
```sh
|
||||
git submodule foreach 'git pull --rebase --tags && git checkout v1.1.0'
|
||||
git submodule foreach 'git pull --rebase --tags && git checkout v1.8.1'
|
||||
```
|
||||
|
||||
3. Запуск контейнеров:
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
volumes:
|
||||
outline:
|
||||
name: outline
|
||||
outline-postgres:
|
||||
name: outline-postgres
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: outline
|
||||
|
||||
services:
|
||||
outline:
|
||||
container_name: outline
|
||||
image: flameshikari/outline-ru:nightly
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.prod
|
||||
args:
|
||||
- APP_PATH=${APP_PATH}
|
||||
- SRC_PATH=${SRC_PATH}
|
||||
network_mode: host
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- outline:/var/lib/outline/data
|
||||
depends_on:
|
||||
- outline-postgres
|
||||
- outline-redis
|
||||
- outline-oidc
|
||||
environment:
|
||||
FILE_STORAGE: local
|
||||
FORCE_HTTPS: false
|
||||
PORT: ${PORT_OUTLINE}
|
||||
URL: http://${ADDRESS}:${PORT_OUTLINE}
|
||||
SECRET_KEY: ${SECRET}
|
||||
UTILS_SECRET: ${SECRET}
|
||||
REDIS_URL: redis://${ADDRESS}:${PORT_REDIS}
|
||||
DATABASE_URL: postgres://${COMMON}:${COMMON}@${ADDRESS}:${PORT_POSTGRES}/${COMMON}
|
||||
PGSSLMODE: disable
|
||||
OIDC_ISSUER_URL: http://${ADDRESS}:${PORT_OIDC}
|
||||
OIDC_CLIENT_ID: ${COMMON}
|
||||
OIDC_CLIENT_SECRET: ${COMMON}
|
||||
|
||||
outline-oidc:
|
||||
container_name: outline-oidc
|
||||
image: ghcr.io/soluto/oidc-server-mock:0.11.0
|
||||
ports:
|
||||
- ${PORT_OIDC}:80
|
||||
healthcheck:
|
||||
test: curl -fs ${ADDRESS}/health || exit 1
|
||||
start_period: 2s
|
||||
interval: 1s
|
||||
timeout: 100ms
|
||||
retries: 10
|
||||
environment:
|
||||
ASPNETCORE_URLS: http://+:80
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
CLIENTS_CONFIGURATION_INLINE: |
|
||||
[
|
||||
{
|
||||
"ClientId": "${COMMON}",
|
||||
"ClientSecrets": ["${COMMON}"],
|
||||
"RedirectUris": ["http://${ADDRESS}:${PORT_OUTLINE}/auth/oidc.callback"],
|
||||
"AllowedGrantTypes": ["authorization_code"],
|
||||
"AllowedScopes": ["openid", "profile", "email"],
|
||||
"RequirePkce": false
|
||||
}
|
||||
]
|
||||
USERS_CONFIGURATION_INLINE: |
|
||||
[
|
||||
{
|
||||
"SubjectId": "1",
|
||||
"Username": "${COMMON}",
|
||||
"Password": "${COMMON}",
|
||||
"Claims": [
|
||||
{
|
||||
"Type": "email",
|
||||
"Value": "mail@example.com",
|
||||
"ValueType": "string"
|
||||
},
|
||||
{
|
||||
"Type": "name",
|
||||
"Value": "Outline",
|
||||
"ValueType": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
SERVER_OPTIONS_INLINE: |
|
||||
{
|
||||
"AccessTokenJwtType": "JWT",
|
||||
"Discovery": {
|
||||
"ShowKeySet": true
|
||||
},
|
||||
"Authentication": {
|
||||
"CookieSameSiteMode": "Lax",
|
||||
"CheckSessionCookieSameSiteMode": "Lax"
|
||||
}
|
||||
}
|
||||
LOGIN_OPTIONS_INLINE: |
|
||||
{
|
||||
"AllowRememberLogin": false
|
||||
}
|
||||
LOGOUT_OPTIONS_INLINE: |
|
||||
{
|
||||
"AutomaticRedirectAfterSignOut": true
|
||||
}
|
||||
ASPNET_SERVICES_OPTIONS_INLINE: |
|
||||
{
|
||||
"ForwardedHeadersOptions": {
|
||||
"ForwardedHeaders" : "All"
|
||||
}
|
||||
}
|
||||
|
||||
outline-redis:
|
||||
container_name: outline-redis
|
||||
image: redis:7
|
||||
ports:
|
||||
- ${PORT_REDIS}:6379
|
||||
healthcheck:
|
||||
test: redis-cli ping
|
||||
interval: 10s
|
||||
timeout: 30s
|
||||
retries: 3
|
||||
|
||||
outline-postgres:
|
||||
container_name: outline-postgres
|
||||
image: postgres:17
|
||||
ports:
|
||||
- ${PORT_POSTGRES}:5432
|
||||
volumes:
|
||||
- outline-postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: pg_isready
|
||||
interval: 30s
|
||||
timeout: 20s
|
||||
retries: 3
|
||||
environment:
|
||||
POSTGRES_USER: ${COMMON}
|
||||
POSTGRES_PASSWORD: ${COMMON}
|
||||
POSTGRES_DB: ${COMMON}
|
||||
+58
-25
@@ -8,33 +8,66 @@ networks:
|
||||
default:
|
||||
name: outline
|
||||
|
||||
x-outline-base: &outline-base
|
||||
container_name: outline
|
||||
depends_on:
|
||||
- outline-services
|
||||
environment:
|
||||
FILE_STORAGE: local
|
||||
FORCE_HTTPS: false
|
||||
PORT: ${PORT:-10240}
|
||||
PORT_VITE: ${PORT_VITE:-10242}
|
||||
URL: http://127.0.0.1:${PORT:-10240}
|
||||
SECRET_KEY: ${SECRET:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}
|
||||
UTILS_SECRET: ${SECRET:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef}
|
||||
REDIS_URL: redis://outline-services
|
||||
DATABASE_URL: postgres://outline:outline@outline-services/outline
|
||||
PGSSLMODE: disable
|
||||
OIDC_ISSUER_URL: http://outline-services:8080
|
||||
OIDC_CLIENT_ID: outline
|
||||
OIDC_CLIENT_SECRET: outline
|
||||
OIDC_DISPLAY_NAME: OIDC
|
||||
|
||||
services:
|
||||
outline:
|
||||
extends:
|
||||
file: docker-compose.prod.yml
|
||||
service: outline
|
||||
image: !reset
|
||||
pull_policy: !reset
|
||||
build:
|
||||
dockerfile: !reset
|
||||
depends_on:
|
||||
- outline-postgres
|
||||
- outline-redis
|
||||
- outline-oidc
|
||||
<<: *outline-base
|
||||
profiles: [prod]
|
||||
build: .
|
||||
image: flameshikari/outline-ru:nightly
|
||||
ports:
|
||||
- ${PORT:-10240}:${PORT:-10240}
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- outline:/var/lib/outline/data
|
||||
|
||||
outline-dev:
|
||||
<<: *outline-base
|
||||
build:
|
||||
dockerfile: Dockerfile.dev
|
||||
profiles: [dev]
|
||||
ports:
|
||||
- ${PORT:-10240}:${PORT:-10240}
|
||||
- ${PORT_VITE:-10242}:${PORT_VITE:-10242}
|
||||
volumes:
|
||||
- outline:/var/lib/outline/data
|
||||
- ./translation/ru.json:/opt/outline/shared/i18n/locales/ru_RU/translation.json
|
||||
|
||||
outline-oidc:
|
||||
extends:
|
||||
file: docker-compose.prod.yml
|
||||
service: outline-oidc
|
||||
|
||||
outline-redis:
|
||||
extends:
|
||||
file: docker-compose.prod.yml
|
||||
service: outline-redis
|
||||
|
||||
outline-postgres:
|
||||
extends:
|
||||
file: docker-compose.prod.yml
|
||||
service: outline-postgres
|
||||
outline-services:
|
||||
container_name: outline-services
|
||||
image: outline-services
|
||||
build: services
|
||||
environment:
|
||||
ISSUER: http://outline-services:8080
|
||||
PUBLIC_URL: http://127.0.0.1:${PORT_OIDC:-10241}
|
||||
volumes:
|
||||
- outline-postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- ${PORT_OIDC:-10241}:8080
|
||||
healthcheck:
|
||||
test: |
|
||||
pg_isready -U outline && \
|
||||
redis-cli ping && \
|
||||
wget -qO- http://127.0.0.1:8080/health
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
+1
-1
Submodule outline updated: 6cfc7da40b...c2edd41e87
@@ -0,0 +1,122 @@
|
||||
diff --git a/app/utils/i18n.ts b/app/utils/i18n.ts
|
||||
index d20fe802a..de4987a99 100644
|
||||
--- a/app/utils/i18n.ts
|
||||
+++ b/app/utils/i18n.ts
|
||||
@@ -51,5 +51,16 @@ export function initI18n(defaultLanguage = "en_US") {
|
||||
Logger.error("Failed to initialize i18n", err);
|
||||
});
|
||||
|
||||
+ // HMR: when a translation JSON changes on disk, the Vite dev server emits
|
||||
+ // an "i18n:update" event (see vite.config.ts). Reload the resources for
|
||||
+ // that language and ask react-i18next to re-render the tree.
|
||||
+ if (typeof import.meta.hot !== "undefined") {
|
||||
+ import.meta.hot.on("i18n:update", async ({ lng: changed }) => {
|
||||
+ const target = unicodeCLDRtoBCP47(changed);
|
||||
+ await i18n.reloadResources(target);
|
||||
+ i18n.emit("languageChanged", i18n.language);
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
return i18n;
|
||||
}
|
||||
diff --git a/server/middlewares/csp.ts b/server/middlewares/csp.ts
|
||||
index 477793da6..93bc7c6e9 100644
|
||||
--- a/server/middlewares/csp.ts
|
||||
+++ b/server/middlewares/csp.ts
|
||||
@@ -59,8 +59,9 @@ export default function createCSPMiddleware(options?: CSPOptions) {
|
||||
|
||||
// Allow to load assets from Vite
|
||||
if (!env.isProduction) {
|
||||
- scriptSrc.push(env.URL.replace(`:${env.PORT}`, ":3001"));
|
||||
- scriptSrc.push("localhost:3001");
|
||||
+ const vitePort = Number(process.env.PORT_VITE) || 3001;
|
||||
+ scriptSrc.push(env.URL.replace(`:${env.PORT}`, `:${vitePort}`));
|
||||
+ scriptSrc.push(`localhost:${vitePort}`);
|
||||
} else {
|
||||
scriptSrc.push(env.URL);
|
||||
}
|
||||
diff --git a/server/routes/app.ts b/server/routes/app.ts
|
||||
index b598752f2..11667c560 100644
|
||||
--- a/server/routes/app.ts
|
||||
+++ b/server/routes/app.ts
|
||||
@@ -22,7 +22,8 @@ import { loadPublicShare } from "@server/commands/shareLoader";
|
||||
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const entry = "app/index.tsx";
|
||||
-const viteHost = env.URL.replace(`:${env.PORT}`, ":3001");
|
||||
+const vitePort = Number(process.env.PORT_VITE) || 3001;
|
||||
+const viteHost = env.URL.replace(`:${env.PORT}`, `:${vitePort}`);
|
||||
|
||||
let indexHtmlCache: Buffer | undefined;
|
||||
|
||||
diff --git a/server/routes/index.ts b/server/routes/index.ts
|
||||
index 0d304ec50..e32ec2da8 100644
|
||||
--- a/server/routes/index.ts
|
||||
+++ b/server/routes/index.ts
|
||||
@@ -101,14 +101,23 @@ router.get("/locales/:lng.json", async (ctx) => {
|
||||
await send(ctx, path.join(lng, "translation.json"), {
|
||||
setHeaders: (res, _, stats) => {
|
||||
res.setHeader("Last-Modified", formatRFC7231(stats.mtime));
|
||||
- res.setHeader("Cache-Control", `public, max-age=${7 * Day.seconds}`);
|
||||
+ res.setHeader(
|
||||
+ "Cache-Control",
|
||||
+ env.isDevelopment
|
||||
+ ? "no-store"
|
||||
+ : `public, max-age=${7 * Day.seconds}`
|
||||
+ );
|
||||
res.setHeader(
|
||||
"ETag",
|
||||
crypto.createHash("md5").update(stats.mtime.toISOString()).digest("hex")
|
||||
);
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
},
|
||||
- root: path.join(__dirname, "../../shared/i18n/locales"),
|
||||
+ // In dev read the source tree directly so the bind-mounted translation
|
||||
+ // files (and HMR'd changes) are seen without rebuilding.
|
||||
+ root: env.isDevelopment
|
||||
+ ? path.join(__dirname, "../../../shared/i18n/locales")
|
||||
+ : path.join(__dirname, "../../shared/i18n/locales"),
|
||||
});
|
||||
});
|
||||
|
||||
diff --git a/vite.config.ts b/vite.config.ts
|
||||
index 829346243..244308bb3 100644
|
||||
--- a/vite.config.ts
|
||||
+++ b/vite.config.ts
|
||||
@@ -31,7 +31,7 @@ export default () =>
|
||||
publicDir: "./server/static",
|
||||
base: (environment.CDN_URL ?? "") + "/static/",
|
||||
server: {
|
||||
- port: 3001,
|
||||
+ port: Number(process.env.PORT_VITE),
|
||||
host: true,
|
||||
https: httpsConfig,
|
||||
allowedHosts: host ? [host] : undefined,
|
||||
@@ -45,6 +45,27 @@ export default () =>
|
||||
: { strict: true },
|
||||
},
|
||||
plugins: [
|
||||
+ {
|
||||
+ // Custom HMR for translation JSON files. Watches shared/i18n/locales
|
||||
+ // and sends a custom WS event so the client can call
|
||||
+ // i18n.reloadResources() instead of doing a full page reload.
|
||||
+ name: "outline-i18n-hmr",
|
||||
+ apply: "serve",
|
||||
+ configureServer(server) {
|
||||
+ const dir = path.resolve(__dirname, "shared/i18n/locales");
|
||||
+ server.watcher.add(dir);
|
||||
+ server.watcher.on("change", (file) => {
|
||||
+ const m = /locales[\\/]([^\\/]+)[\\/]translation\.json$/.exec(file);
|
||||
+ if (m) {
|
||||
+ server.ws.send({
|
||||
+ type: "custom",
|
||||
+ event: "i18n:update",
|
||||
+ data: { lng: m[1] },
|
||||
+ });
|
||||
+ }
|
||||
+ });
|
||||
+ },
|
||||
+ },
|
||||
react(),
|
||||
// https://vite-pwa-org.netlify.app/
|
||||
VitePWA({
|
||||
@@ -1,34 +0,0 @@
|
||||
diff --git a/server/routes/index.ts b/server/routes/index.ts
|
||||
index 26fbded27..478de1922 100644
|
||||
--- a/server/routes/index.ts
|
||||
+++ b/server/routes/index.ts
|
||||
@@ -103,7 +103,7 @@ router.get("/locales/:lng.json", async (ctx) => {
|
||||
await send(ctx, path.join(lng, "translation.json"), {
|
||||
setHeaders: (res, _, stats) => {
|
||||
res.setHeader("Last-Modified", formatRFC7231(stats.mtime));
|
||||
- res.setHeader("Cache-Control", `public, max-age=${7 * Day.seconds}`);
|
||||
+ res.setHeader("Cache-Control", "no-store");
|
||||
res.setHeader(
|
||||
"ETag",
|
||||
crypto.createHash("md5").update(stats.mtime.toISOString()).digest("hex")
|
||||
diff --git a/vite.config.ts b/vite.config.ts
|
||||
index 32b52d44e..26d611fee 100644
|
||||
--- a/vite.config.ts
|
||||
+++ b/vite.config.ts
|
||||
@@ -45,6 +45,17 @@ export default () =>
|
||||
: { strict: true },
|
||||
},
|
||||
plugins: [
|
||||
+ {
|
||||
+ name: 'reload',
|
||||
+ configureServer(server) {
|
||||
+ const { ws, watcher } = server;
|
||||
+ watcher.on('change', file => {
|
||||
+ if (file.endsWith('build/shared/i18n/locales/ru_RU/translation.json')) {
|
||||
+ ws.send({ type: 'full-reload' });
|
||||
+ }
|
||||
+ });
|
||||
+ },
|
||||
+ },
|
||||
react(),
|
||||
// https://vite-pwa-org.netlify.app/
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM oven/bun:1-alpine
|
||||
|
||||
USER root
|
||||
|
||||
RUN apk add --no-cache postgresql17 postgresql17-contrib redis su-exec bash
|
||||
|
||||
ENV PGDATA=/var/lib/postgresql/data
|
||||
RUN mkdir -p "$PGDATA" /run/postgresql /var/log \
|
||||
&& chown -R postgres:postgres "$PGDATA" /run/postgresql
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN bun install --production
|
||||
COPY server.js entrypoint.sh avatar.png ./
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
CMD ["./entrypoint.sh"]
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
PG_USER="${POSTGRES_USER:-${COMMON:-outline}}"
|
||||
PG_PASS="${POSTGRES_PASSWORD:-${COMMON:-outline}}"
|
||||
PG_DB="${POSTGRES_DB:-${COMMON:-outline}}"
|
||||
|
||||
# named-volume mount comes up root-owned; reclaim it for postgres
|
||||
chown -R postgres:postgres "$PGDATA" /run/postgresql
|
||||
chmod 700 "$PGDATA"
|
||||
|
||||
# ---- Postgres ----
|
||||
if [ ! -s "$PGDATA/PG_VERSION" ]; then
|
||||
echo "==> initdb in $PGDATA"
|
||||
su-exec postgres initdb -D "$PGDATA" --username="$PG_USER" \
|
||||
--auth-local=trust --auth-host=md5 >/dev/null
|
||||
echo "listen_addresses = '*'" >> "$PGDATA/postgresql.conf"
|
||||
echo "host all all 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"
|
||||
fi
|
||||
|
||||
echo "==> starting postgres on :5432"
|
||||
su-exec postgres pg_ctl -D "$PGDATA" -l "$PGDATA/postgres.log" -w start
|
||||
|
||||
# ensure password is set (md5 auth needs it for host connections)
|
||||
su-exec postgres psql -U "$PG_USER" -d postgres -c \
|
||||
"ALTER ROLE \"$PG_USER\" WITH LOGIN PASSWORD '$PG_PASS';" >/dev/null
|
||||
|
||||
# create db idempotently
|
||||
su-exec postgres psql -U "$PG_USER" -d postgres -tAc \
|
||||
"SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 || \
|
||||
su-exec postgres createdb -U "$PG_USER" -O "$PG_USER" "$PG_DB"
|
||||
|
||||
# ---- Redis ----
|
||||
echo "==> starting redis on :6379"
|
||||
redis-server --daemonize yes --bind 0.0.0.0 --port 6379 \
|
||||
--logfile "" --protected-mode no
|
||||
|
||||
# ---- OIDC mock (foreground) ----
|
||||
echo "==> starting bun OIDC mock on :${PORT_OIDC:-8080}"
|
||||
exec bun server.js
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mock-oidc",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.0",
|
||||
"jose": "^5.9.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
// Mock OIDC server with autologin to a single user.
|
||||
// Compatible with IdentityServer-style clients (/connect/*) and generic OIDC clients.
|
||||
// NOT FOR PRODUCTION.
|
||||
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import crypto from 'crypto';
|
||||
import { SignJWT, exportJWK, generateKeyPair } from 'jose';
|
||||
|
||||
// ---------- Config ----------
|
||||
// ISSUER = internal URL used by server-side relying parties (outline backend),
|
||||
// also signed into JWT `iss` claim.
|
||||
// PUBLIC = browser-facing URL (host:published-port) used in redirect endpoints.
|
||||
// PORT = derived from ISSUER URL so a single var sets where we listen.
|
||||
const ISSUER = process.env.ISSUER || 'http://localhost:8080';
|
||||
const PUBLIC = process.env.PUBLIC_URL || ISSUER;
|
||||
const PORT = parseInt(new URL(ISSUER).port || '8080', 10);
|
||||
const CLIENT_ID = process.env.CLIENT_ID || 'mock-client';
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET || 'mock-secret';
|
||||
const TOKEN_TTL = parseInt(process.env.TOKEN_TTL || '3600', 10);
|
||||
const CODE_TTL = parseInt(process.env.CODE_TTL || '60', 10);
|
||||
|
||||
const USER = {
|
||||
sub: process.env.USER_SUB || 'user-1',
|
||||
email: process.env.USER_EMAIL || 'mail@example.com',
|
||||
email_verified: true,
|
||||
name: process.env.USER_NAME || 'Outline',
|
||||
preferred_username: process.env.USER_USERNAME || 'outline',
|
||||
given_name: process.env.USER_GIVEN || 'Outline',
|
||||
family_name: process.env.USER_FAMILY || 'Wiki',
|
||||
roles: (process.env.USER_ROLES || 'admin,user').split(','),
|
||||
picture: `${PUBLIC}/avatar.png`,
|
||||
};
|
||||
|
||||
// ---------- Keys ----------
|
||||
const { publicKey, privateKey } = await generateKeyPair('RS256');
|
||||
const jwk = { ...(await exportJWK(publicKey)), kid: 'mock-key-1', alg: 'RS256', use: 'sig' };
|
||||
|
||||
// ---------- In-memory stores ----------
|
||||
const codes = new Map();
|
||||
const tokens = new Map();
|
||||
const refreshTokens = new Map();
|
||||
|
||||
// ---------- Helpers ----------
|
||||
const now = () => Math.floor(Date.now() / 1000);
|
||||
|
||||
const signJwt = (payload, audience, ttl = TOKEN_TTL) =>
|
||||
new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: 'RS256', kid: jwk.kid, typ: 'JWT' })
|
||||
.setIssuer(ISSUER)
|
||||
.setSubject(USER.sub)
|
||||
.setAudience(audience)
|
||||
.setIssuedAt(now())
|
||||
.setExpirationTime(now() + ttl)
|
||||
.setJti(crypto.randomBytes(16).toString('hex'))
|
||||
.sign(privateKey);
|
||||
|
||||
const extractClientCreds = (req) => {
|
||||
const auth = req.headers.authorization;
|
||||
if (auth?.startsWith('Basic ')) {
|
||||
const decoded = Buffer.from(auth.slice(6), 'base64').toString();
|
||||
const idx = decoded.indexOf(':');
|
||||
return {
|
||||
clientId: decodeURIComponent(decoded.slice(0, idx)),
|
||||
clientSecret: decodeURIComponent(decoded.slice(idx + 1)),
|
||||
};
|
||||
}
|
||||
return { clientId: req.body.client_id, clientSecret: req.body.client_secret };
|
||||
};
|
||||
|
||||
const pkceMatches = (verifier, challenge, method) => {
|
||||
if (!verifier) return false;
|
||||
if (method === 'S256') return crypto.createHash('sha256').update(verifier).digest('base64url') === challenge;
|
||||
return verifier === challenge;
|
||||
};
|
||||
|
||||
// ---------- App ----------
|
||||
const app = express();
|
||||
app.use(cors({ origin: true, credentials: true }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
||||
next();
|
||||
});
|
||||
|
||||
// ---------- Discovery + JWKS ----------
|
||||
app.get('/.well-known/openid-configuration', (_req, res) => res.json({
|
||||
issuer: ISSUER,
|
||||
authorization_endpoint: `${PUBLIC}/connect/authorize`,
|
||||
token_endpoint: `${ISSUER}/connect/token`,
|
||||
userinfo_endpoint: `${ISSUER}/connect/userinfo`,
|
||||
end_session_endpoint: `${PUBLIC}/connect/endsession`,
|
||||
jwks_uri: `${ISSUER}/.well-known/jwks`,
|
||||
response_types_supported: ['code'],
|
||||
response_modes_supported: ['query', 'fragment'],
|
||||
subject_types_supported: ['public'],
|
||||
id_token_signing_alg_values_supported: ['RS256'],
|
||||
scopes_supported: ['openid', 'profile', 'email', 'offline_access'],
|
||||
token_endpoint_auth_methods_supported: ['client_secret_post', 'client_secret_basic', 'none'],
|
||||
claims_supported: ['sub', 'email', 'email_verified', 'name', 'preferred_username', 'given_name', 'family_name', 'roles'],
|
||||
code_challenge_methods_supported: ['S256', 'plain'],
|
||||
grant_types_supported: ['authorization_code', 'refresh_token'],
|
||||
}));
|
||||
|
||||
app.get(['/.well-known/jwks', '/.well-known/openid-configuration/jwks', '/jwks'],
|
||||
(_req, res) => res.json({ keys: [jwk] }));
|
||||
|
||||
// ---------- /authorize: AUTOLOGIN ----------
|
||||
app.get(['/connect/authorize', '/authorize'], (req, res) => {
|
||||
const { redirect_uri, state, nonce, scope, response_type,
|
||||
client_id, code_challenge, code_challenge_method } = req.query;
|
||||
const fail = (status, error, description) =>
|
||||
res.status(status).json({ error, error_description: description });
|
||||
|
||||
if (response_type !== 'code') return fail(400, 'unsupported_response_type', 'response_type must be "code"');
|
||||
if (!client_id) return fail(400, 'invalid_request', 'client_id is required');
|
||||
if (!redirect_uri) return fail(400, 'invalid_request', 'redirect_uri is required');
|
||||
|
||||
const code = crypto.randomBytes(32).toString('hex');
|
||||
codes.set(code, {
|
||||
client_id, redirect_uri, nonce,
|
||||
scope: scope || 'openid',
|
||||
code_challenge, code_challenge_method,
|
||||
expires_at: Date.now() + CODE_TTL * 1000,
|
||||
});
|
||||
|
||||
const url = new URL(redirect_uri);
|
||||
url.searchParams.set('code', code);
|
||||
if (state) url.searchParams.set('state', state);
|
||||
|
||||
console.log(` -> autologin: redirect to ${url.toString()}`);
|
||||
res.redirect(url.toString());
|
||||
});
|
||||
|
||||
// ---------- /token ----------
|
||||
app.post(['/connect/token', '/token'], async (req, res) => {
|
||||
const { grant_type } = req.body;
|
||||
|
||||
if (grant_type === 'authorization_code') {
|
||||
const { code, redirect_uri, code_verifier } = req.body;
|
||||
const { clientId, clientSecret } = extractClientCreds(req);
|
||||
|
||||
const ctx = codes.get(code);
|
||||
codes.delete(code);
|
||||
if (!ctx) return res.status(400).json({ error: 'invalid_grant', error_description: 'unknown code' });
|
||||
if (ctx.expires_at < Date.now()) return res.status(400).json({ error: 'invalid_grant', error_description: 'code expired' });
|
||||
if (ctx.redirect_uri !== redirect_uri) return res.status(400).json({ error: 'invalid_grant', error_description: 'redirect_uri mismatch' });
|
||||
|
||||
if (ctx.code_challenge) {
|
||||
if (!pkceMatches(code_verifier, ctx.code_challenge, ctx.code_challenge_method))
|
||||
return res.status(400).json({ error: 'invalid_grant', error_description: 'PKCE verification failed' });
|
||||
} else if (clientId !== CLIENT_ID || clientSecret !== CLIENT_SECRET) {
|
||||
return res.status(401).json({ error: 'invalid_client' });
|
||||
}
|
||||
|
||||
const aud = clientId || CLIENT_ID;
|
||||
const id_token = await signJwt({ ...USER, nonce: ctx.nonce, auth_time: now() }, aud);
|
||||
const access_token = await signJwt({ scope: ctx.scope, client_id: aud, ...USER }, aud);
|
||||
tokens.set(access_token, USER);
|
||||
|
||||
const response = { access_token, id_token, token_type: 'Bearer', expires_in: TOKEN_TTL, scope: ctx.scope };
|
||||
if (ctx.scope.split(' ').includes('offline_access')) {
|
||||
const refresh_token = crypto.randomBytes(32).toString('hex');
|
||||
refreshTokens.set(refresh_token, { client_id: aud, scope: ctx.scope });
|
||||
response.refresh_token = refresh_token;
|
||||
}
|
||||
return res.json(response);
|
||||
}
|
||||
|
||||
if (grant_type === 'refresh_token') {
|
||||
const { refresh_token } = req.body;
|
||||
const ctx = refreshTokens.get(refresh_token);
|
||||
if (!ctx) return res.status(400).json({ error: 'invalid_grant' });
|
||||
|
||||
const id_token = await signJwt({ ...USER, auth_time: now() }, ctx.client_id);
|
||||
const access_token = await signJwt({ scope: ctx.scope, client_id: ctx.client_id, ...USER }, ctx.client_id);
|
||||
tokens.set(access_token, USER);
|
||||
|
||||
return res.json({
|
||||
access_token, id_token, refresh_token,
|
||||
token_type: 'Bearer', expires_in: TOKEN_TTL, scope: ctx.scope,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(400).json({ error: 'unsupported_grant_type' });
|
||||
});
|
||||
|
||||
// ---------- /userinfo ----------
|
||||
app.all(['/connect/userinfo', '/userinfo'], (req, res) => {
|
||||
const auth = req.headers.authorization;
|
||||
const user = auth?.startsWith('Bearer ') ? tokens.get(auth.slice(7)) : null;
|
||||
if (!user) return res.status(401).json({ error: 'invalid_token' });
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
// ---------- /endsession ----------
|
||||
app.get(['/connect/endsession', '/logout'], (req, res) => {
|
||||
const { post_logout_redirect_uri, state } = req.query;
|
||||
if (post_logout_redirect_uri) {
|
||||
const url = new URL(post_logout_redirect_uri);
|
||||
if (state) url.searchParams.set('state', state);
|
||||
return res.redirect(url.toString());
|
||||
}
|
||||
res.type('html').send('<h1>Logged out</h1>');
|
||||
});
|
||||
|
||||
// ---------- Index + health + 404 ----------
|
||||
app.get('/', (_req, res) => res.type('html').send(`<h1>OIDC Mock Server</h1>
|
||||
<p>Autologin as <strong>${USER.email}</strong> (sub: <code>${USER.sub}</code>)</p>
|
||||
<ul>
|
||||
<li><a href="/.well-known/openid-configuration">/.well-known/openid-configuration</a></li>
|
||||
<li><a href="/.well-known/jwks">/.well-known/jwks</a></li>
|
||||
<li><a href="/connect/authorize?response_type=code&client_id=${CLIENT_ID}&redirect_uri=http://localhost:3000/callback&scope=openid+profile+email&state=test">Simulate /authorize</a></li>
|
||||
</ul>
|
||||
<p>Client ID: <code>${CLIENT_ID}</code> · Client Secret: <code>${CLIENT_SECRET}</code></p>`));
|
||||
|
||||
app.get('/avatar.png', (_req, res) => res.sendFile('avatar.png', { root: import.meta.dirname }));
|
||||
|
||||
app.get('/health', (_req, res) => res.json({ status: 'ok', issuer: ISSUER }));
|
||||
|
||||
app.use((req, res) => res.status(404).json({
|
||||
error: 'not_found', path: req.path,
|
||||
hint: 'See /.well-known/openid-configuration',
|
||||
}));
|
||||
|
||||
// ---------- Start ----------
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Mock OIDC listening on :${PORT} issuer=${ISSUER} public=${PUBLIC} client_id=${CLIENT_ID} autologin=${USER.email} (sub=${USER.sub}, roles=${USER.roles.join(',')})`);
|
||||
});
|
||||
+735
-322
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user