1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-14 17:02:27 +00:00

refactor!: serve the static frontend trough the backend (#520)

Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
Elias Schneider
2025-05-17 00:36:58 +02:00
parent bf710aec56
commit f8a7467ec0
74 changed files with 773 additions and 819 deletions

View File

@@ -5,8 +5,7 @@
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"features": { "features": {
"ghcr.io/devcontainers/features/go:1": {}, "ghcr.io/devcontainers/features/go:1": {}
"ghcr.io/devcontainers-extra/features/caddy:1": {}
}, },
"customizations": { "customizations": {
"vscode": { "vscode": {

View File

@@ -6,6 +6,9 @@ node_modules
/frontend/.svelte-kit /frontend/.svelte-kit
/frontend/build /frontend/build
/backend/bin /backend/bin
/backend/frontend/dist
/frontend/tests/.auth
/frontend/tests/.report
# Env # Env
@@ -15,4 +18,5 @@ node_modules
# Application specific # Application specific
data data
/scripts/development /scripts/development
/backend/GeoLite2-City.mmdb

View File

@@ -1,5 +1,5 @@
# See the documentation for more information: https://pocket-id.org/docs/configuration/environment-variables # See the documentation for more information: https://pocket-id.org/docs/configuration/environment-variables
PUBLIC_APP_URL=http://localhost APP_URL=http://localhost:1411
TRUST_PROXY=false TRUST_PROXY=false
MAXMIND_LICENSE_KEY= MAXMIND_LICENSE_KEY=
PUID=1000 PUID=1000

View File

@@ -35,5 +35,6 @@ jobs:
uses: golangci/golangci-lint-action@dec74fa03096ff515422f71d18d41307cacde373 # v7.0.0 uses: golangci/golangci-lint-action@dec74fa03096ff515422f71d18d41307cacde373 # v7.0.0
with: with:
version: v2.0.2 version: v2.0.2
args: --build-tags=exclude_frontend
working-directory: backend working-directory: backend
only-new-issues: ${{ github.event_name == 'pull_request' }} only-new-issues: ${{ github.event_name == 'pull_request' }}

View File

@@ -1,50 +0,0 @@
name: Build and Push Docker Image
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-22.04 # Using an older version because of https://github.com/actions/runner-images/issues/11471
permissions:
contents: read
packages: write
steps:
- name: checkout code
uses: actions/checkout@v3
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},prefix=v
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 'Login to GitHub Container Registry'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{github.repository_owner}}
password: ${{secrets.GITHUB_TOKEN}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -56,7 +56,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: 22
cache: "npm" cache: "npm"
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
@@ -116,7 +116,7 @@ jobs:
run: | run: |
docker run -d --name pocket-id-sqlite \ docker run -d --name pocket-id-sqlite \
--network pocket-id-network \ --network pocket-id-network \
-p 80:80 \ -p 1411:1411 \
-e APP_ENV=test \ -e APP_ENV=test \
pocket-id:test pocket-id:test
@@ -155,7 +155,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: lts/* node-version: 22
cache: "npm" cache: "npm"
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
@@ -253,7 +253,7 @@ jobs:
run: | run: |
docker run -d --name pocket-id-postgres \ docker run -d --name pocket-id-postgres \
--network pocket-id-network \ --network pocket-id-network \
-p 80:80 \ -p 1411:1411 \
-e APP_ENV=test \ -e APP_ENV=test \
-e DB_PROVIDER=postgres \ -e DB_PROVIDER=postgres \
-e DB_CONNECTION_STRING=postgresql://postgres:postgres@pocket-id-db:5432/pocket-id \ -e DB_CONNECTION_STRING=postgresql://postgres:postgres@pocket-id-db:5432/pocket-id \

84
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
build-docker-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}},prefix=v
type=semver,pattern={{major}}.{{minor}},prefix=v
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: "Login to GitHub Container Registry"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{github.repository_owner}}
password: ${{secrets.GITHUB_TOKEN}}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-binaries:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install frontend dependencies
working-directory: frontend
run: npm ci
- name: Build frontend
working-directory: frontend
run: npm run build
- name: Build binaries
run: sh scripts/development/build-binaries.sh
- name: Upload binaries to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${{ github.ref_name }} backend/.bin/*
publish-release:
runs-on: ubuntu-latest
needs: [build-docker-image, build-binaries]
permissions:
contents: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Mark release as published
run: gh release edit ${{ github.ref_name }} --draft=false

View File

@@ -39,7 +39,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "lts/*" node-version: 22
cache: "npm" cache: "npm"
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json

View File

@@ -29,7 +29,7 @@ jobs:
working-directory: backend working-directory: backend
run: | run: |
set -e -o pipefail set -e -o pipefail
go test -v ./... | tee /tmp/TestResults.log go test -tags=exclude_frontend -v ./... | tee /tmp/TestResults.log
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: always() if: always()
with: with:

View File

@@ -25,6 +25,7 @@ jobs:
run: | run: |
mkdir -p backend/resources mkdir -p backend/resources
jq -c 'map_values(.name)' data.json > backend/resources/aaguids.json jq -c 'map_values(.name)' data.json > backend/resources/aaguids.json
rm data.json
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7

2
.gitignore vendored
View File

@@ -30,6 +30,7 @@ vite.config.ts.timestamp-*
*.dll *.dll
*.so *.so
*.dylib *.dylib
/backend/.bin
# Application specific # Application specific
data data
@@ -37,6 +38,7 @@ data
/frontend/tests/.report /frontend/tests/.report
pocket-id-backend pocket-id-backend
/backend/GeoLite2-City.mmdb /backend/GeoLite2-City.mmdb
/backend/frontend/dist
# Misc # Misc
.DS_Store .DS_Store

37
.vscode/tasks.json vendored
View File

@@ -1,37 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Run Caddy",
"type": "shell",
"command": "caddy run --config reverse-proxy/Caddyfile",
"isBackground": true,
"problemMatcher": {
"owner": "custom",
"pattern": [
{
"regexp": ".",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "Caddyfile.*"
}
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"runOptions": {
"runOn": "folderOpen",
"instanceLimit": 1
}
}
]
}

View File

@@ -31,9 +31,11 @@ Before you submit the pull request for review please ensure that
- You run `npm run format` to format the code - You run `npm run format` to format the code
## Setup project ## Setup project
Pocket ID consists of a frontend, backend and a reverse proxy. There are two ways to get the development environment setup: Pocket ID consists of a frontend, backend and a reverse proxy. There are two ways to get the development environment setup:
## 1. Using DevContainers ## 1. Using DevContainers
1. Make sure you have [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension installed 1. Make sure you have [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension installed
2. Clone and open the repo in VS Code 2. Clone and open the repo in VS Code
3. VS Code will detect .devcontainer and will prompt you to open the folder in devcontainer 3. VS Code will detect .devcontainer and will prompt you to open the folder in devcontainer
@@ -48,8 +50,8 @@ The backend is built with [Gin](https://gin-gonic.com) and written in Go.
#### Setup #### Setup
1. Open the `backend` folder 1. Open the `backend` folder
2. Copy the `.env.example` file to `.env` and change the `APP_ENV` to `development` 2. Copy the `.env.example` file to `.env` and edit the variables as needed
3. Start the backend with `go run -tags e2etest ./cmd` 3. Start the backend with `go run -tags e2etest,exclude_frontend ./cmd`
### Frontend ### Frontend
@@ -58,27 +60,18 @@ The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in Ty
#### Setup #### Setup
1. Open the `frontend` folder 1. Open the `frontend` folder
2. Copy the `.env.example` file to `.env` 2. Copy the `.env.example` file to `.env` and edit the variables as needed
3. Install the dependencies with `npm install` 3. Install the dependencies with `npm install`
4. Start the frontend with `npm run dev` 4. Start the frontend with `npm run dev`
### Reverse Proxy You're all set! The application is now listening on `localhost:3000`. The backend gets proxied trough the frontend in development mode.
We use [Caddy](https://caddyserver.com) as a reverse proxy. You can use any other reverse proxy if you want but you have to configure it yourself.
#### Setup
Run `caddy run --config reverse-proxy/Caddyfile` in the root folder.
You're all set!
## Debugging
1. The VS Code is currently setup to auto launch caddy on opening the folder. (Defined in [tasks.json](.vscode/tasks.json))
2. Press `F5` to start a debug session. This will launch both frontend and backend and attach debuggers to those process. (Defined in [launch.json](.vscode/launch.json))
### Testing ### Testing
We are using [Playwright](https://playwright.dev) for end-to-end testing. We are using [Playwright](https://playwright.dev) for end-to-end testing.
The tests can be run like this: The tests can be run like this:
1. Start the backend normally 1. Start the backend normally
2. Start the frontend in production mode with `npm run build && node --env-file=.env build/index.js` 2. Start the frontend in production mode with `npm run build && node --env-file=.env build/index.js`
3. Run the tests with `npm run test` 3. Run the tests with `npm run test`

View File

@@ -1,55 +1,50 @@
# Tags passed to "go build" # Tags passed to "go build"
ARG BUILD_TAGS="" ARG BUILD_TAGS=""
ARG VERSION="unknown"
# Stage 1: Build Frontend # Stage 1: Build Frontend
FROM node:22-alpine AS frontend-builder FROM node:22-alpine AS frontend-builder
WORKDIR /app/frontend WORKDIR /build
COPY ./frontend/package*.json ./ COPY ./frontend/package*.json ./
RUN npm ci RUN npm ci
COPY ./frontend ./ COPY ./frontend ./
RUN npm run build RUN BUILD_OUTPUT_PATH=dist npm run build
RUN npm prune --production
# Stage 2: Build Backend # Stage 2: Build Backend
FROM golang:1.24-alpine AS backend-builder FROM golang:1.24-alpine AS backend-builder
ARG BUILD_TAGS ARG BUILD_TAGS
WORKDIR /app/backend WORKDIR /build
COPY ./backend/go.mod ./backend/go.sum ./ COPY ./backend/go.mod ./backend/go.sum ./
RUN go mod download RUN go mod download
RUN apk add --no-cache gcc musl-dev
COPY ./backend ./ COPY ./backend ./
WORKDIR /app/backend/cmd COPY --from=frontend-builder /build/dist ./frontend/dist
RUN CGO_ENABLED=0 \ COPY .version .version
WORKDIR /build/cmd
RUN VERSION=$(cat /build/.version) \
CGO_ENABLED=0 \
GOOS=linux \ GOOS=linux \
go build \ go build \
-tags "${BUILD_TAGS}" \ -tags "${BUILD_TAGS}" \
-ldflags="-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${VERSION}" \ -ldflags="-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${VERSION}" \
-o /app/backend/pocket-id-backend \ -o /build/pocket-id-backend \
. .
# Stage 3: Production Image # Stage 3: Production Image
FROM node:22-alpine FROM alpine
# Delete default node user
RUN deluser --remove-home node
RUN apk add --no-cache caddy curl su-exec
COPY ./reverse-proxy /etc/caddy/
WORKDIR /app WORKDIR /app
COPY --from=frontend-builder /app/frontend/build ./frontend/build
COPY --from=frontend-builder /app/frontend/node_modules ./frontend/node_modules
COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend RUN apk add --no-cache curl su-exec
COPY ./scripts ./scripts COPY --from=backend-builder /build/pocket-id-backend /app/pocket-id
RUN find ./scripts -name "*.sh" -exec chmod +x {} \; COPY ./scripts/docker /app/docker
COPY ./scripts/create-one-time-access-token.sh /app/
EXPOSE 80 RUN chmod +x /app/pocket-id /app/create-one-time-access-token.sh && \
find /app/docker -name "*.sh" -exec chmod +x {} \;
EXPOSE 1411
ENV APP_ENV=production ENV APP_ENV=production
ENTRYPOINT ["sh", "./scripts/docker/create-user.sh"] ENTRYPOINT ["sh", "/app/docker/entrypoint.sh"]
CMD ["sh", "./scripts/docker/entrypoint.sh"] CMD ["/app/pocket-id"]

View File

@@ -1,10 +1,7 @@
APP_ENV=production # Sample .env file for development
PUBLIC_APP_URL=http://localhost # All environment variables can be found on https://pocket-id.org/docs/configuration/environment-variables
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
DB_PROVIDER=sqlite APP_ENV=development
# MAXMIND_LICENSE_KEY=fixme # needed for IP geolocation in the audit log APP_URL=http://localhost:1411
SQLITE_DB_PATH=data/pocket-id.db PORT=1411
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id MAXMIND_LICENSE_KEY=your_license_key
UPLOAD_PATH=data/uploads
PORT=8080
HOST=0.0.0.0

View File

@@ -0,0 +1,9 @@
//go:build exclude_frontend
package frontend
import "github.com/gin-gonic/gin"
func RegisterFrontend(router *gin.Engine) error {
return ErrFrontendNotIncluded
}

View File

@@ -0,0 +1,77 @@
//go:build !exclude_frontend
package frontend
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
)
//go:embed all:dist/*
var frontendFS embed.FS
func RegisterFrontend(router *gin.Engine) error {
distFS, err := fs.Sub(frontendFS, "dist")
if err != nil {
return fmt.Errorf("failed to create sub FS: %w", err)
}
cacheMaxAge := time.Hour * 24
fileServer := NewFileServerWithCaching(http.FS(distFS), int(cacheMaxAge.Seconds()))
router.NoRoute(func(c *gin.Context) {
// Try to serve the requested file
path := strings.TrimPrefix(c.Request.URL.Path, "/")
if _, err := fs.Stat(distFS, path); os.IsNotExist(err) {
// File doesn't exist, serve index.html instead
c.Request.URL.Path = "/"
}
fileServer.ServeHTTP(c.Writer, c.Request)
})
return nil
}
// FileServerWithCaching wraps http.FileServer to add caching headers
type FileServerWithCaching struct {
root http.FileSystem
lastModified time.Time
cacheMaxAge int
lastModifiedHeaderValue string
cacheControlHeaderValue string
}
func NewFileServerWithCaching(root http.FileSystem, maxAge int) *FileServerWithCaching {
return &FileServerWithCaching{
root: root,
lastModified: time.Now(),
cacheMaxAge: maxAge,
lastModifiedHeaderValue: time.Now().UTC().Format(http.TimeFormat),
cacheControlHeaderValue: fmt.Sprintf("public, max-age=%d", maxAge),
}
}
func (f *FileServerWithCaching) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check if the client has a cached version
if ifModifiedSince := r.Header.Get("If-Modified-Since"); ifModifiedSince != "" {
ifModifiedSinceTime, err := time.Parse(http.TimeFormat, ifModifiedSince)
if err == nil && f.lastModified.Before(ifModifiedSinceTime.Add(1*time.Second)) {
// Client's cached version is up to date
w.WriteHeader(http.StatusNotModified)
return
}
}
w.Header().Set("Last-Modified", f.lastModifiedHeaderValue)
w.Header().Set("Cache-Control", f.cacheControlHeaderValue)
http.FileServer(f.root).ServeHTTP(w, r)
}

View File

@@ -0,0 +1,5 @@
package frontend
import "errors"
var ErrFrontendNotIncluded = errors.New("frontend is not included")

View File

@@ -2,12 +2,15 @@ package bootstrap
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log" "log"
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/pocket-id/pocket-id/backend/frontend"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
"golang.org/x/time/rate" "golang.org/x/time/rate"
@@ -45,6 +48,10 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
r := gin.Default() r := gin.Default()
r.Use(gin.Logger()) r.Use(gin.Logger())
if !common.EnvConfig.TrustProxy {
_ = r.SetTrustedProxies(nil)
}
if common.EnvConfig.TracingEnabled { if common.EnvConfig.TracingEnabled {
r.Use(otelgin.Middleware("pocket-id-backend")) r.Use(otelgin.Middleware("pocket-id-backend"))
} }
@@ -55,6 +62,13 @@ func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) {
r.Use(middleware.NewCorsMiddleware().Add()) r.Use(middleware.NewCorsMiddleware().Add())
r.Use(middleware.NewErrorHandlerMiddleware().Add()) r.Use(middleware.NewErrorHandlerMiddleware().Add())
err := frontend.RegisterFrontend(r)
if errors.Is(err, frontend.ErrFrontendNotIncluded) {
log.Println("Frontend is not included in the build. Skipping frontend registration.")
} else if err != nil {
return nil, fmt.Errorf("failed to register frontend: %w", err)
}
// Initialize middleware for specific routes // Initialize middleware for specific routes
authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService) authMiddleware := middleware.NewAuthMiddleware(svc.apiKeyService, svc.userService, svc.jwtService)
fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware() fileSizeLimitMiddleware := middleware.NewFileSizeLimitMiddleware()

View File

@@ -25,19 +25,20 @@ const (
type EnvConfigSchema struct { type EnvConfigSchema struct {
AppEnv string `env:"APP_ENV"` AppEnv string `env:"APP_ENV"`
AppURL string `env:"PUBLIC_APP_URL"` AppURL string `env:"APP_URL"`
DbProvider DbProvider `env:"DB_PROVIDER"` DbProvider DbProvider `env:"DB_PROVIDER"`
DbConnectionString string `env:"DB_CONNECTION_STRING"` DbConnectionString string `env:"DB_CONNECTION_STRING"`
UploadPath string `env:"UPLOAD_PATH"` UploadPath string `env:"UPLOAD_PATH"`
KeysPath string `env:"KEYS_PATH"` KeysPath string `env:"KEYS_PATH"`
Port string `env:"BACKEND_PORT"` Port string `env:"PORT"`
Host string `env:"HOST"` Host string `env:"HOST"`
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"` MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
GeoLiteDBPath string `env:"GEOLITE_DB_PATH"` GeoLiteDBPath string `env:"GEOLITE_DB_PATH"`
GeoLiteDBUrl string `env:"GEOLITE_DB_URL"` GeoLiteDBUrl string `env:"GEOLITE_DB_URL"`
UiConfigDisabled bool `env:"PUBLIC_UI_CONFIG_DISABLED"` UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"`
MetricsEnabled bool `env:"METRICS_ENABLED"` MetricsEnabled bool `env:"METRICS_ENABLED"`
TracingEnabled bool `env:"TRACING_ENABLED"` TracingEnabled bool `env:"TRACING_ENABLED"`
TrustProxy bool `env:"TRUST_PROXY"`
} }
var EnvConfig = &EnvConfigSchema{ var EnvConfig = &EnvConfigSchema{
@@ -46,8 +47,8 @@ var EnvConfig = &EnvConfigSchema{
DbConnectionString: "file:data/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate", DbConnectionString: "file:data/pocket-id.db?_pragma=journal_mode(WAL)&_pragma=busy_timeout(2500)&_txlock=immediate",
UploadPath: "data/uploads", UploadPath: "data/uploads",
KeysPath: "data/keys", KeysPath: "data/keys",
AppURL: "http://localhost", AppURL: "http://localhost:1411",
Port: "8080", Port: "1411",
Host: "0.0.0.0", Host: "0.0.0.0",
MaxMindLicenseKey: "", MaxMindLicenseKey: "",
GeoLiteDBPath: "data/GeoLite2-City.mmdb", GeoLiteDBPath: "data/GeoLite2-City.mmdb",
@@ -55,6 +56,7 @@ var EnvConfig = &EnvConfigSchema{
UiConfigDisabled: false, UiConfigDisabled: false,
MetricsEnabled: false, MetricsEnabled: false,
TracingEnabled: false, TracingEnabled: false,
TrustProxy: false,
} }
func init() { func init() {
@@ -78,9 +80,9 @@ func init() {
parsedAppUrl, err := url.Parse(EnvConfig.AppURL) parsedAppUrl, err := url.Parse(EnvConfig.AppURL)
if err != nil { if err != nil {
log.Fatal("PUBLIC_APP_URL is not a valid URL") log.Fatal("APP_URL is not a valid URL")
} }
if parsedAppUrl.Path != "" { if parsedAppUrl.Path != "" {
log.Fatal("PUBLIC_APP_URL must not contain a path") log.Fatal("APP_URL must not contain a path")
} }
} }

View File

@@ -68,6 +68,13 @@ func (acc *AppConfigController) listAppConfigHandler(c *gin.Context) {
return return
} }
// Manually add uiConfigDisabled which isn't in the database but defined with an environment variable
configVariablesDto = append(configVariablesDto, dto.PublicAppConfigVariableDto{
Key: "uiConfigDisabled",
Value: strconv.FormatBool(common.EnvConfig.UiConfigDisabled),
Type: "boolean",
})
c.JSON(http.StatusOK, configVariablesDto) c.JSON(http.StatusOK, configVariablesDto)
} }

View File

@@ -4,12 +4,12 @@ services:
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
ports: ports:
- 3000:80 - 1411:1411
volumes: volumes:
- "./data:/app/backend/data" - "./data:/app/data"
# Optional healthcheck # Optional healthcheck
healthcheck: healthcheck:
test: "curl -f http://localhost/healthz" test: "curl -f http://localhost:1411/healthz"
interval: 1m30s interval: 1m30s
timeout: 5s timeout: 5s
retries: 2 retries: 2

View File

@@ -1,3 +1,2 @@
PUBLIC_APP_URL=http://localhost # If the backend in your development environment is running on a different port, change the value of the variable below.
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS DEVELOPMENT_BACKEND_URL=http://localhost:1411
INTERNAL_BACKEND_URL=http://localhost:8080

View File

@@ -1,12 +1,12 @@
{ {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "0.51.0", "version": "0.53.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pocket-id-frontend", "name": "pocket-id-frontend",
"version": "0.51.0", "version": "0.53.0",
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/browser": "^13.1.0",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
@@ -30,8 +30,7 @@
"@inlang/plugin-message-format": "^4.0.0", "@inlang/plugin-message-format": "^4.0.0",
"@internationalized/date": "^3.7.0", "@internationalized/date": "^3.7.0",
"@playwright/test": "^1.50.0", "@playwright/test": "^1.50.0",
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.20.7", "@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",
@@ -1019,98 +1018,6 @@
"node": ">=18.16.0" "node": ">=18.16.0"
} }
}, },
"node_modules/@rollup/plugin-commonjs": {
"version": "28.0.2",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz",
"integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"commondir": "^1.0.1",
"estree-walker": "^2.0.2",
"fdir": "^6.2.0",
"is-reference": "1.2.1",
"magic-string": "^0.30.3",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=16.0.0 || 14 >= 14.17"
},
"peerDependencies": {
"rollup": "^2.68.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz",
"integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.0.1",
"@types/resolve": "1.20.2",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.22.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^2.78.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^2.0.2",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.40.1", "version": "4.40.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
@@ -1413,33 +1320,16 @@
"sqlite-wasm": "bin/index.js" "sqlite-wasm": "bin/index.js"
} }
}, },
"node_modules/@sveltejs/adapter-auto": { "node_modules/@sveltejs/adapter-static": {
"version": "4.0.0", "version": "3.0.8",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.8.tgz",
"integrity": "sha512-kmuYSQdD2AwThymQF0haQhM8rE5rhutQXG4LNbnbShwhMO4qQGnKaaTy+88DuNSuoQDi58+thpq8XpHc1+oEKQ==", "integrity": "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg==",
"dev": true, "dev": true,
"dependencies": { "license": "MIT",
"import-meta-resolve": "^4.1.0"
},
"peerDependencies": { "peerDependencies": {
"@sveltejs/kit": "^2.0.0" "@sveltejs/kit": "^2.0.0"
} }
}, },
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"dev": true,
"dependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"rollup": "^4.9.5"
},
"peerDependencies": {
"@sveltejs/kit": "^2.4.0"
}
},
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.20.7", "version": "2.20.7",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.7.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.7.tgz",
@@ -1770,12 +1660,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/resolve": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
"dev": true
},
"node_modules/@types/validator": { "node_modules/@types/validator": {
"version": "13.12.2", "version": "13.12.2",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
@@ -2410,12 +2294,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
"dev": true
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2910,12 +2788,6 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"node_modules/esutils": { "node_modules/esutils": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -3154,15 +3026,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": { "node_modules/get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -3226,18 +3089,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/human-id": { "node_modules/human-id": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz", "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.1.1.tgz",
@@ -3291,21 +3142,6 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3336,12 +3172,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
"dev": true
},
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -3351,15 +3181,6 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"dependencies": {
"@types/estree": "*"
}
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3997,12 +3818,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -4424,26 +4239,6 @@
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"dependencies": {
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": { "node_modules/resolve-from": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -4691,18 +4486,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svelte": { "node_modules/svelte": {
"version": "5.19.3", "version": "5.19.3",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.3.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.19.3.tgz",

View File

@@ -35,8 +35,7 @@
"@inlang/plugin-message-format": "^4.0.0", "@inlang/plugin-message-format": "^4.0.0",
"@internationalized/date": "^3.7.0", "@internationalized/date": "^3.7.0",
"@playwright/test": "^1.50.0", "@playwright/test": "^1.50.0",
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-static": "^3.0.8",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.20.7", "@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/eslint": "^9.6.1", "@types/eslint": "^9.6.1",

View File

@@ -15,7 +15,7 @@ export default defineConfig({
? [['html', { outputFolder: 'tests/.report' }], ['github']] ? [['html', { outputFolder: 'tests/.report' }], ['github']]
: [['line'], ['html', { open: 'never', outputFolder: 'tests/.report' }]], : [['line'], ['html', { open: 'never', outputFolder: 'tests/.report' }]],
use: { use: {
baseURL: 'http://localhost', baseURL: process.env.APP_URL ?? 'http://localhost:1411',
video: 'retain-on-failure', video: 'retain-on-failure',
trace: 'on-first-retry' trace: 'on-first-retry'
}, },

View File

@@ -1,88 +0,0 @@
import { env } from '$env/dynamic/private';
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import { paraglideMiddleware } from '$lib/paraglide/server';
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { AxiosError } from 'axios';
import { decodeJwt } from 'jose';
// Workaround so that we can also import this environment variable into client-side code
// If we would directly import $env/dynamic/private into the api-service.ts file, it would throw an error
// this is still secure as process will just be undefined in the browser
process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080';
// Handle to use the paraglide middleware
const paraglideHandle: Handle = ({ event, resolve }) => {
return paraglideMiddleware(event.request, ({ locale }) => {
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', locale)
});
});
};
const authenticationHandle: Handle = async ({ event, resolve }) => {
const { isSignedIn, isAdmin } = verifyJwt(event.cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const path = event.url.pathname;
const isUnauthenticatedOnlyPath =
path == '/login' || path.startsWith('/login/') || path == '/lc' || path.startsWith('/lc/');
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
if (!isUnauthenticatedOnlyPath && !isPublicPath && !isSignedIn) {
return new Response(null, {
status: 303,
headers: { location: '/login' }
});
}
if (isUnauthenticatedOnlyPath && isSignedIn) {
return new Response(null, {
status: 303,
headers: { location: '/settings' }
});
}
if (isAdminPath && !isAdmin) {
return new Response(null, {
status: 303,
headers: { location: '/settings' }
});
}
return resolve(event);
};
export const handle: Handle = sequence(paraglideHandle, authenticationHandle);
export const handleError: HandleServerError = async ({ error, message, status }) => {
if (error instanceof AxiosError) {
message = error.response?.data.error || message;
status = error.response?.status || status;
console.error(
`Axios error: ${error.request.path} - ${error.response?.data.error ?? error.message}`
);
} else {
console.error(error);
}
return {
message,
status
};
};
function verifyJwt(accessToken: string | undefined) {
let isSignedIn = false;
let isAdmin = false;
if (accessToken) {
const jwtPayload = decodeJwt<{ isAdmin: boolean }>(accessToken);
if (jwtPayload?.exp && jwtPayload.exp * 1000 > Date.now()) {
isSignedIn = true;
isAdmin = !!jwtPayload?.isAdmin;
}
}
return { isSignedIn, isAdmin };
}

31
frontend/src/hooks.ts Normal file
View File

@@ -0,0 +1,31 @@
import { paraglideMiddleware } from '$lib/paraglide/server';
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { AxiosError } from 'axios';
// Handle to use the paraglide middleware
const paraglideHandle: Handle = ({ event, resolve }) => {
return paraglideMiddleware(event.request, ({ locale }) => {
return resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', locale)
});
});
};
export const handle: Handle = paraglideHandle;
export const handleError: HandleServerError = async ({ error, message, status }) => {
if (error instanceof AxiosError) {
message = error.response?.data.error || message;
status = error.response?.status || status;
console.error(
`Axios error: ${error.request.path} - ${error.response?.data.error ?? error.message}`
);
} else {
console.error(error);
}
return {
message,
status
};
};

View File

@@ -1,2 +0,0 @@
export const HTTPS_ENABLED = process.env.PUBLIC_APP_URL?.startsWith('https://') ?? false;
export const ACCESS_TOKEN_COOKIE_NAME = HTTPS_ENABLED ? '__Host-access_token' : 'access_token';

View File

@@ -1,19 +1,13 @@
import { browser } from '$app/environment';
import axios from 'axios'; import axios from 'axios';
abstract class APIService { abstract class APIService {
api = axios.create({ api = axios.create({
withCredentials: true baseURL: '/api'
}); });
constructor(accessToken?: string) { constructor() {
if (accessToken) { if (typeof process !== 'undefined' && process?.env?.DEVELOPMENT_BACKEND_URL) {
this.api.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; this.api.defaults.baseURL = process.env.DEVELOPMENT_BACKEND_URL;
}
if (browser) {
this.api.defaults.baseURL = '/api';
} else {
this.api.defaults.baseURL = process!.env!.INTERNAL_BACKEND_URL + '/api';
} }
} }
} }

View File

@@ -1,6 +1,4 @@
import { version as currentVersion } from '$app/environment';
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration'; import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
import axios from 'axios';
import APIService from './api-service'; import APIService from './api-service';
export default class AppConfigService extends APIService { export default class AppConfigService extends APIService {
@@ -55,28 +53,6 @@ export default class AppConfigService extends APIService {
await this.api.post('/application-configuration/sync-ldap'); await this.api.post('/application-configuration/sync-ldap');
} }
async getVersionInformation() {
const response = await axios
.get('https://api.github.com/repos/pocket-id/pocket-id/releases/latest', {
timeout: 2000
})
.then((res) => res.data)
.catch(() => null);
let newestVersion: string | undefined;
let isUpToDate: boolean | undefined;
if (response) {
newestVersion = response.tag_name.replace('v', '');
isUpToDate = newestVersion === currentVersion;
}
return {
isUpToDate,
newestVersion,
currentVersion
};
}
private parseConfigList(data: AppConfigRawResponse) { private parseConfigList(data: AppConfigRawResponse) {
const appConfig: Partial<AllAppConfig> = {}; const appConfig: Partial<AllAppConfig> = {};
data.forEach(({ key, value }) => { data.forEach(({ key, value }) => {

View File

@@ -0,0 +1,109 @@
import { version as currentVersion } from '$app/environment';
import axios from 'axios';
const VERSION_CACHE_KEY = 'version_cache';
const CACHE_DURATION = 2 * 60 * 60 * 1000; // 2 hours
async function getNewestVersion() {
const cachedData = await getVersionFromCache();
// If we have valid cached data, return it
if (cachedData) {
return cachedData;
}
// Otherwise fetch from API
try {
const response = await axios
.get('https://api.github.com/repos/pocket-id/pocket-id/releases/latest', {
timeout: 2000
})
.then((res) => res.data);
console.log('Fetched newest version:', response);
const newestVersion = response.tag_name.replace('v', '');
// Cache the result
cacheVersion(newestVersion);
return newestVersion;
} catch (error) {
console.error('Failed to fetch newest version:', error);
// If fetch fails but we have an expired cache, return that as fallback
const cache = getCacheObject();
return cache?.newestVersion || currentVersion;
}
}
function getCurrentVersion() {
return currentVersion;
}
async function isUpToDate() {
const newestVersion = await getNewestVersion();
const currentVersion = getCurrentVersion();
// If the current version changed, invalidate the cache
const cache = getCacheObject();
if (cache?.lastCurrentVersion && currentVersion !== cache.lastCurrentVersion) {
invalidateCache();
}
return newestVersion === currentVersion;
}
// Helper methods for caching
function getCacheObject() {
const cacheJson = localStorage.getItem(VERSION_CACHE_KEY);
if (!cacheJson) return null;
try {
return JSON.parse(cacheJson);
} catch (e) {
console.error('Failed to parse cache:', e);
return null;
}
}
async function getVersionFromCache() {
const cache = getCacheObject();
if (!cache || !cache.newestVersion || !cache.timestamp) {
return null;
}
const now = Date.now();
// Check if cache is still valid
if (now - cache.timestamp > CACHE_DURATION) {
invalidateCache();
return null;
}
// Check if current version matches what it was when we cached
if (cache.lastCurrentVersion && cache.lastCurrentVersion !== currentVersion) {
invalidateCache();
return null;
}
return cache.newestVersion;
}
async function cacheVersion(version :string) {
const cacheObject = {
newestVersion: version,
timestamp: Date.now(),
lastCurrentVersion: currentVersion
};
localStorage.setItem(VERSION_CACHE_KEY, JSON.stringify(cacheObject));
}
async function invalidateCache() {
localStorage.removeItem(VERSION_CACHE_KEY);
}
export default {
getNewestVersion,
getCurrentVersion,
isUpToDate
};

View File

@@ -5,6 +5,7 @@ export type AppConfig = {
emailOneTimeAccessAsAdminEnabled: boolean; emailOneTimeAccessAsAdminEnabled: boolean;
ldapEnabled: boolean; ldapEnabled: boolean;
disableAnimations: boolean; disableAnimations: boolean;
uiConfigDisabled: boolean;
}; };
export type AllAppConfig = AppConfig & { export type AllAppConfig = AppConfig & {
@@ -49,7 +50,7 @@ export type AppConfigRawResponse = {
}[]; }[];
export type AppVersionInformation = { export type AppVersionInformation = {
isUpToDate?: boolean; isUpToDate: boolean | null;
newestVersion?: string; newestVersion: string | null;
currentVersion: string; currentVersion: string;
}; };

View File

@@ -1,5 +1,3 @@
import { browser } from '$app/environment';
type SkipCacheUntil = { type SkipCacheUntil = {
[key: string]: number; [key: string]: number;
}; };
@@ -9,14 +7,12 @@ export function getProfilePictureUrl(userId?: string) {
let url = `/api/users/${userId}/profile-picture.png`; let url = `/api/users/${userId}/profile-picture.png`;
if (browser) { const skipCacheUntil = getSkipCacheUntil(userId);
const skipCacheUntil = getSkipCacheUntil(userId); const skipCache = skipCacheUntil > Date.now();
const skipCache = skipCacheUntil > Date.now(); if (skipCache) {
if (skipCache) { const skipCacheParam = new URLSearchParams();
const skipCacheParam = new URLSearchParams(); skipCacheParam.append('skip-cache', skipCacheUntil.toString());
skipCacheParam.append('skip-cache', skipCacheUntil.toString()); url += '?' + skipCacheParam.toString();
url += '?' + skipCacheParam.toString();
}
} }
return url.toString(); return url.toString();

View File

@@ -1,26 +0,0 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import AppConfigService from '$lib/services/app-config-service';
import UserService from '$lib/services/user-service';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ cookies }) => {
const accessToken = cookies.get(ACCESS_TOKEN_COOKIE_NAME);
const userService = new UserService(accessToken);
const appConfigService = new AppConfigService(accessToken);
const userPromise = userService.getCurrent().catch(() => null);
const appConfigPromise = appConfigService.list().catch((e) => {
console.error(
`Failed to get application configuration: ${e.response?.data.error || e.message}`
);
return null;
});
const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]);
return {
user,
appConfig
};
};

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment';
import ConfirmDialog from '$lib/components/confirm-dialog/confirm-dialog.svelte'; import ConfirmDialog from '$lib/components/confirm-dialog/confirm-dialog.svelte';
import Error from '$lib/components/error.svelte'; import Error from '$lib/components/error.svelte';
import Header from '$lib/components/header/header.svelte'; import Header from '$lib/components/header/header.svelte';
@@ -22,9 +21,10 @@
const { user, appConfig } = data; const { user, appConfig } = data;
if (browser && user) { if (user) {
userStore.setUser(user); userStore.setUser(user);
} }
if (appConfig) { if (appConfig) {
appConfigStore.set(appConfig); appConfigStore.set(appConfig);
} }

View File

@@ -0,0 +1,55 @@
import { goto } from '$app/navigation';
import AppConfigService from '$lib/services/app-config-service';
import UserService from '$lib/services/user-service';
import type { User } from '$lib/types/user.type';
import type { LayoutLoad } from './$types';
export const ssr = false;
export const load: LayoutLoad = async ({ url }) => {
const userService = new UserService();
const appConfigService = new AppConfigService();
const userPromise = userService.getCurrent().catch(() => null);
const appConfigPromise = appConfigService.list().catch((e) => {
console.error(
`Failed to get application configuration: ${e.response?.data.error || e.message}`
);
return null;
});
const [user, appConfig] = await Promise.all([userPromise, appConfigPromise]);
const redirectPath = await getRedirectPath(url.pathname, user);
if (redirectPath) {
goto(redirectPath);
}
return {
user,
appConfig
};
};
const getRedirectPath = async (path: string, user: User | null) => {
const isSignedIn = !!user;
const isAdmin = user?.isAdmin;
const isUnauthenticatedOnlyPath =
path == '/login' || path.startsWith('/login/') || path == '/lc' || path.startsWith('/lc/');
const isPublicPath = ['/authorize', '/device', '/health', '/healthz'].includes(path);
const isAdminPath = path == '/settings/admin' || path.startsWith('/settings/admin/');
if (!isUnauthenticatedOnlyPath && !isPublicPath && !isSignedIn) {
return '/login';
}
if (isUnauthenticatedOnlyPath && isSignedIn) {
return '/settings';
}
if (isAdminPath && !isAdmin) {
return '/settings';
}
};

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import OidcService from '$lib/services/oidc-service'; import OidcService from '$lib/services/oidc-service';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ url, cookies }) => { export const load: PageLoad = async ({ url }) => {
const clientId = url.searchParams.get('client_id'); const clientId = url.searchParams.get('client_id');
const oidcService = new OidcService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const oidcService = new OidcService();
const client = await oidcService.getClientMetaData(clientId!); const client = await oidcService.getClientMetaData(clientId!);

View File

@@ -1,9 +0,0 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ url }) => {
const code = url.searchParams.get('code');
return {
code
};
};

View File

@@ -0,0 +1,9 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
const code = url.searchParams.get('code');
return {
code
};
};

View File

@@ -1,37 +0,0 @@
import { version as currentVersion } from '$app/environment';
import { env } from '$env/dynamic/private';
import AppConfigService from '$lib/services/app-config-service';
import type { AppVersionInformation } from '$lib/types/application-configuration';
import type { LayoutServerLoad } from './$types';
let versionInformation: AppVersionInformation;
let versionInformationLastUpdated: number;
export const load: LayoutServerLoad = async () => {
if (env.UPDATE_CHECK_DISABLED === 'true') {
return {
versionInformation: {
currentVersion: currentVersion
} satisfies AppVersionInformation
};
}
const appConfigService = new AppConfigService();
// Cache the version information for 3 hours
const cacheExpired =
versionInformationLastUpdated &&
Date.now() - versionInformationLastUpdated > 1000 * 60 * 60 * 3;
if (!versionInformation || cacheExpired) {
versionInformation = await appConfigService.getVersionInformation();
if (versionInformation.newestVersion == null) {
console.error('Failed to fetch version information. Trying again in 3 hours.');
}
versionInformationLastUpdated = Date.now();
}
return {
versionInformation
};
};

View File

@@ -0,0 +1,17 @@
import versionService from '$lib/services/version-service';
import type { AppVersionInformation } from '$lib/types/application-configuration';
import type { LayoutLoad } from './$types';
export const prerender = false;
export const load: LayoutLoad = async () => {
const versionInformation: AppVersionInformation = {
currentVersion: versionService.getCurrentVersion(),
newestVersion: await versionService.getNewestVersion(),
isUpToDate: await versionService.isUpToDate()
};
return {
versionInformation
};
};

View File

@@ -1,20 +0,0 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import UserService from '$lib/services/user-service';
import WebAuthnService from '$lib/services/webauthn-service';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => {
const accessToken = cookies.get(ACCESS_TOKEN_COOKIE_NAME);
const webauthnService = new WebAuthnService(accessToken);
const userService = new UserService(accessToken);
const [account, passkeys] = await Promise.all([
userService.getCurrent(),
webauthnService.listCredentials()
]);
return {
account,
passkeys
};
};

View File

@@ -0,0 +1,18 @@
import UserService from '$lib/services/user-service';
import WebAuthnService from '$lib/services/webauthn-service';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
const webauthnService = new WebAuthnService();
const userService = new UserService();
const [account, passkeys] = await Promise.all([
userService.getCurrent(),
webauthnService.listCredentials()
]);
return {
account,
passkeys
};
};

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import ApiKeyService from '$lib/services/api-key-service'; import ApiKeyService from '$lib/services/api-key-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageLoad = async () => {
const apiKeyService = new ApiKeyService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const apiKeyService = new ApiKeyService();
const apiKeysRequestOptions: SearchPaginationSortRequest = { const apiKeysRequestOptions: SearchPaginationSortRequest = {
sort: { sort: {

View File

@@ -1,9 +0,0 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import AppConfigService from '$lib/services/app-config-service';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => {
const appConfigService = new AppConfigService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const appConfig = await appConfigService.list(true);
return { appConfig };
};

View File

@@ -0,0 +1,8 @@
import AppConfigService from '$lib/services/app-config-service';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
const appConfigService = new AppConfigService();
const appConfig = await appConfigService.list(true);
return { appConfig };
};

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { env } from '$env/dynamic/public';
import { openConfirmDialog } from '$lib/components/confirm-dialog'; import { openConfirmDialog } from '$lib/components/confirm-dialog';
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte'; import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
@@ -8,6 +7,7 @@
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import AppConfigService from '$lib/services/app-config-service'; import AppConfigService from '$lib/services/app-config-service';
import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration'; import type { AllAppConfig } from '$lib/types/application-configuration';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -22,7 +22,6 @@
} = $props(); } = $props();
const appConfigService = new AppConfigService(); const appConfigService = new AppConfigService();
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
const tlsOptions = { const tlsOptions = {
none: 'None', none: 'None',
starttls: 'StartTLS', starttls: 'StartTLS',
@@ -96,7 +95,7 @@
</script> </script>
<form onsubmit={onSubmit}> <form onsubmit={onSubmit}>
<fieldset disabled={uiConfigDisabled}> <fieldset disabled={$appConfigStore.uiConfigDisabled}>
<h4 class="text-lg font-semibold">{m.smtp_configuration()}</h4> <h4 class="text-lg font-semibold">{m.smtp_configuration()}</h4>
<div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2"> <div class="mt-4 grid grid-cols-1 items-end gap-5 md:grid-cols-2">
<FormInput label={m.smtp_host()} bind:input={$inputs.smtpHost} /> <FormInput label={m.smtp_host()} bind:input={$inputs.smtpHost} />
@@ -160,6 +159,6 @@
<Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail} <Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail}
>{m.send_test_email()}</Button >{m.send_test_email()}</Button
> >
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button> <Button type="submit" disabled={$appConfigStore.uiConfigDisabled}>{m.save()}</Button>
</div> </div>
</form> </form>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { env } from '$env/dynamic/public';
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte'; import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration'; import type { AllAppConfig } from '$lib/types/application-configuration';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
@@ -17,7 +17,6 @@
callback: (appConfig: Partial<AllAppConfig>) => Promise<void>; callback: (appConfig: Partial<AllAppConfig>) => Promise<void>;
} = $props(); } = $props();
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
let isLoading = $state(false); let isLoading = $state(false);
const updatedAppConfig = { const updatedAppConfig = {
@@ -47,7 +46,7 @@
</script> </script>
<form onsubmit={onSubmit}> <form onsubmit={onSubmit}>
<fieldset class="flex flex-col gap-5" disabled={uiConfigDisabled}> <fieldset class="flex flex-col gap-5" disabled={$appConfigStore.uiConfigDisabled}>
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
<FormInput label={m.application_name()} bind:input={$inputs.appName} /> <FormInput label={m.application_name()} bind:input={$inputs.appName} />
<FormInput <FormInput

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { env } from '$env/dynamic/public';
import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte'; import CheckboxWithLabel from '$lib/components/form/checkbox-with-label.svelte';
import FormInput from '$lib/components/form/form-input.svelte'; import FormInput from '$lib/components/form/form-input.svelte';
import { Button } from '$lib/components/ui/button'; import { Button } from '$lib/components/ui/button';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import AppConfigService from '$lib/services/app-config-service'; import AppConfigService from '$lib/services/app-config-service';
import appConfigStore from '$lib/stores/application-configuration-store';
import type { AllAppConfig } from '$lib/types/application-configuration'; import type { AllAppConfig } from '$lib/types/application-configuration';
import { axiosErrorToast } from '$lib/utils/error-util'; import { axiosErrorToast } from '$lib/utils/error-util';
import { createForm } from '$lib/utils/form-util'; import { createForm } from '$lib/utils/form-util';
@@ -20,7 +20,6 @@
} = $props(); } = $props();
const appConfigService = new AppConfigService(); const appConfigService = new AppConfigService();
const uiConfigDisabled = env.PUBLIC_UI_CONFIG_DISABLED === 'true';
let ldapEnabled = $state(appConfig.ldapEnabled); let ldapEnabled = $state(appConfig.ldapEnabled);
let ldapSyncing = $state(false); let ldapSyncing = $state(false);
@@ -106,7 +105,7 @@
<form onsubmit={onSubmit}> <form onsubmit={onSubmit}>
<h4 class="text-lg font-semibold">{m.client_configuration()}</h4> <h4 class="text-lg font-semibold">{m.client_configuration()}</h4>
<fieldset disabled={uiConfigDisabled}> <fieldset disabled={$appConfigStore.uiConfigDisabled}>
<div class="mt-4 grid grid-cols-1 items-start gap-5 md:grid-cols-2"> <div class="mt-4 grid grid-cols-1 items-start gap-5 md:grid-cols-2">
<FormInput <FormInput
label={m.ldap_url()} label={m.ldap_url()}
@@ -215,13 +214,13 @@
<div class="mt-8 flex flex-wrap justify-end gap-3"> <div class="mt-8 flex flex-wrap justify-end gap-3">
{#if ldapEnabled} {#if ldapEnabled}
<Button variant="secondary" onclick={onDisable} disabled={uiConfigDisabled} <Button variant="secondary" onclick={onDisable} disabled={$appConfigStore.uiConfigDisabled}
>{m.disable()}</Button >{m.disable()}</Button
> >
<Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>{m.sync_now()}</Button> <Button variant="secondary" onclick={syncLdap} isLoading={ldapSyncing}>{m.sync_now()}</Button>
<Button type="submit" disabled={uiConfigDisabled}>{m.save()}</Button> <Button type="submit" disabled={$appConfigStore.uiConfigDisabled}>{m.save()}</Button>
{:else} {:else}
<Button onclick={onEnable} disabled={uiConfigDisabled}>{m.enable()}</Button> <Button onclick={onEnable} disabled={$appConfigStore.uiConfigDisabled}>{m.enable()}</Button>
{/if} {/if}
</div> </div>
</form> </form>

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import OIDCService from '$lib/services/oidc-service'; import OIDCService from '$lib/services/oidc-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageLoad = async () => {
const oidcService = new OIDCService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const oidcService = new OIDCService();
const clientsRequestOptions: SearchPaginationSortRequest = { const clientsRequestOptions: SearchPaginationSortRequest = {
sort: { sort: {

View File

@@ -1,8 +0,0 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import OidcService from '$lib/services/oidc-service';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, cookies }) => {
const oidcService = new OidcService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
return await oidcService.getClient(params.id);
};

View File

@@ -0,0 +1,7 @@
import OidcService from '$lib/services/oidc-service';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const oidcService = new OidcService();
return await oidcService.getClient(params.id);
};

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import UserGroupService from '$lib/services/user-group-service'; import UserGroupService from '$lib/services/user-group-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageLoad = async () => {
const userGroupService = new UserGroupService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const userGroupService = new UserGroupService();
const userGroupsRequestOptions: SearchPaginationSortRequest = { const userGroupsRequestOptions: SearchPaginationSortRequest = {
sort: { sort: {

View File

@@ -1,10 +0,0 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import UserGroupService from '$lib/services/user-group-service';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, cookies }) => {
const userGroupService = new UserGroupService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const userGroup = await userGroupService.get(params.id);
return { userGroup };
};

View File

@@ -0,0 +1,9 @@
import UserGroupService from '$lib/services/user-group-service';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const userGroupService = new UserGroupService();
const userGroup = await userGroupService.get(params.id);
return { userGroup };
};

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import UserService from '$lib/services/user-service'; import UserService from '$lib/services/user-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageLoad = async () => {
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const userService = new UserService();
const usersRequestOptions: SearchPaginationSortRequest = { const usersRequestOptions: SearchPaginationSortRequest = {
sort: { sort: {

View File

@@ -1,12 +0,0 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import UserService from '$lib/services/user-service';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, cookies }) => {
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const user = await userService.get(params.id);
return {
user
};
};

View File

@@ -0,0 +1,11 @@
import UserService from '$lib/services/user-service';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const userService = new UserService();
const user = await userService.get(params.id);
return {
user
};
};

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import AuditLogService from '$lib/services/audit-log-service'; import AuditLogService from '$lib/services/audit-log-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageLoad = async () => {
const auditLogService = new AuditLogService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const auditLogService = new AuditLogService();
const auditLogsRequestOptions: SearchPaginationSortRequest = { const auditLogsRequestOptions: SearchPaginationSortRequest = {
sort: { sort: {
column: 'createdAt', column: 'createdAt',

View File

@@ -1,10 +1,9 @@
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import AuditLogService from '$lib/services/audit-log-service'; import AuditLogService from '$lib/services/audit-log-service';
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { PageServerLoad } from './$types'; import type { PageLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => { export const load: PageLoad = async () => {
const auditLogService = new AuditLogService(cookies.get(ACCESS_TOKEN_COOKIE_NAME)); const auditLogService = new AuditLogService();
const requestOptions: SearchPaginationSortRequest = { const requestOptions: SearchPaginationSortRequest = {
sort: { sort: {

View File

@@ -1,4 +1,4 @@
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import packageJson from './package.json' with { type: 'json' }; import packageJson from './package.json' with { type: 'json' };
@@ -12,11 +12,14 @@ const config = {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter(), adapter: adapter({
fallback: "index.html",
pages: process.env.BUILD_OUTPUT_PATH ?? "../backend/frontend/dist",
}),
version: { version: {
name: packageJson.version name: packageJson.version
} },
} },
}; };
export default config; export default config;

View File

@@ -84,33 +84,3 @@ export const refreshTokens = [
expired: true expired: true
} }
]; ];
export const idTokens = [
{
token:
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiSldUIn0.eyJhdWQiOiIzNjU0YTc0Ni0zNWQ0LTQzMjEtYWM2MS0wYmRjZmYyYjQwNTUiLCJlbWFpbCI6InRpbS5jb29rQHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MTY5MDAwMDAwMSwiZmFtaWx5X25hbWUiOiJUaW0iLCJnaXZlbl9uYW1lIjoiQ29vayIsImlhdCI6MTY5MDAwMDAwMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsIm5hbWUiOiJUaW0gQ29vayIsIm5vbmNlIjoib1cxQTFPNzhHUTE1RDczT3NIRXg3V1FLajdacXZITFp1XzM3bWRYSXFBUSIsInN1YiI6IjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIiwidHlwZSI6ImlkLXRva2VuIn0.noxQ-sCNHh7f8EaySJT7oF0DlmjYcM-FdMPH45Yuuvt5-bTpLLkggN9aq8RILmkGL9xUVsfZbYkWV5EkGobxfIoXITE98xH54BQwtpOjLL_HZLF4kFXarUyGLGO3zeVJAQzyofVz_1rKfDlZdi5Zmm-91cO5OiOtshfluDqt1h1D-E5h4ShT0eN7apvSvQnD7806-3tfxP0GHE-HuerR1Qbv9p0uWmuhT0CkVIM-K2dKBHdhLtquRqxNp2EuD_T-HA3WJgvkTTWp-JZ6NqvWDMy3M-jB-_Bs9eABERlTSTp7H2XCMGbwRSBZDmSn-97LPwc-NO5JYEkgZOeVr_r6qg',
clientId: oidcClients.nextcloud.id,
expired: true
},
{
token:
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiSldUIn0.eyJhdWQiOiIzNjU0YTc0Ni0zNWQ0LTQzMjEtYWM2MS0wYmRjZmYyYjQwNTUiLCJlbWFpbCI6InRpbS5jb29rQHRlc3QuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImV4cCI6MjY5MDAwMDAwMSwiZmFtaWx5X25hbWUiOiJUaW0iLCJnaXZlbl9uYW1lIjoiQ29vayIsImlhdCI6MTY5MDAwMDAwMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdCIsIm5hbWUiOiJUaW0gQ29vayIsIm5vbmNlIjoib1cxQTFPNzhHUTE1RDczT3NIRXg3V1FLajdacXZITFp1XzM3bWRYSXFBUSIsInN1YiI6IjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIiwidHlwZSI6ImlkLXRva2VuIn0.ry7s3xP4-vvvZPzcRCvR1yBl32Pi09ryC6Z-67E1P4xChe8MaMoiQgImS5ZNbZiYzBN4cdkQsExXZK1FP-kMD019k3uNKPq0fIREBwrT9wXPqQJlLSBmN-tVkjLm90-b310SG5p65aajWvMkcPmJleG6y24_zoPFr3ISGI87vV6zdyoqG55pc-GkT7FwiEFIZJGQAzl7u1uOi7sQrda8Y6rF_SCC-f9I4PnHblnaTne8pfXe9jXKJeY1ZKj2Qh9dRPhWCLPHHV1YErUyoMP9oeMVzYpno-pBYVOiT9Ktl6CpG-jqB8smKqDEhZrSejgZ256h34f8jNL1SEhpM-4_cQ',
clientId: oidcClients.nextcloud.id,
expired: false
}
];
export const accessTokens = [
{
token:
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiSldUIn0.eyJhdWQiOlsiMzY1NGE3NDYtMzVkNC00MzIxLWFjNjEtMGJkY2ZmMmI0MDU1Il0sImV4cCI6MTc0Mzk3MjI4MywiaWF0IjoxNzQzOTY4NjgzLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Iiwic3ViIjoiZjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIiwidHlwZSI6Im9hdXRoLWFjY2Vzcy10b2tlbiJ9.REFSDFsGso9u7WxpyMmMVvjQMgulbidQNUft-kBRg7nw5LN9pOWhO0Zlr1tZnnrA1LenZRv0BvLIf0qekwGEC4FOPmJ6-As2ggIcoBIXpUR2A4Hhuy0FtqbCUgIkda1Dcx9w1Rmfzi0eHY_-1H_98rDgS5RxqweNA_YP3RsnJqBsc9GYhDarrf1nyCOplshGOEiyisUGoU2TaURI6DTcCiDzVOm_esZqokoZTpKlQw6ZugDDObro0eWYgROo97_3cqPRgRjSYBYRAGCHhZom3bFkjmz3wqpeoGmUNgL022x3-gl7QjurpJMQrKJ7wkFs0bh2uFnnngnh2w6m4j8-5w',
clientId: oidcClients.nextcloud.id,
expired: true
},
{
token:
'eyJhbGciOiJSUzI1NiIsImtpZCI6Ijh1SER3M002cmY4IiwidHlwIjoiSldUIn0.eyJhdWQiOlsiMzY1NGE3NDYtMzVkNC00MzIxLWFjNjEtMGJkY2ZmMmI0MDU1Il0sImV4cCI6Mjc0Mzk3MjI4MywiaWF0IjoxNzQzOTY4NjgzLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Iiwic3ViIjoiZjRiODlkYzItNjJmYi00NmJmLTlmNWYtYzM0ZjRlYWZlOTNlIiwidHlwZSI6Im9hdXRoLWFjY2Vzcy10b2tlbiJ9.FaFsHJS_8wbvQvctftNTPyzAe9IhbpJiHIkhg28RrFRFfnBMq0QycmTUh00MJPXkUfd_j5tcCnXybF1efHsq6WbP4AWFG_TJMUyz7a9SYt1lGR8dxo3eys0YAX5eJQ5YoVTKNrivSKrC37Rg3VlcZVWXp6KBAxRWVl3OUlquSC6q7HNKAKg8sbBJiGpUJ37wwanOTE2XhYGvFB2_gxS36tvOuSTV3CVg_7Fctej7gNhKMXBFMJiIFurxZaeNud8620xtv-vJX6ALa1Qu1SkWhhZN2Yx3WuODZNlni3rUps-THoEdqh62jNwItE9wB7C0fGEKuUqVIllaF9I_7i2s3w',
clientId: oidcClients.nextcloud.id,
expired: false
}
];

View File

@@ -1,6 +1,7 @@
import test, { expect } from '@playwright/test'; import test, { expect } from '@playwright/test';
import { accessTokens, idTokens, oidcClients, refreshTokens, users } from './data'; import { oidcClients, refreshTokens, users } from './data';
import { cleanupBackend } from './utils/cleanup.util'; import { cleanupBackend } from './utils/cleanup.util';
import { generateIdToken, generateOauthAccessToken } from './utils/jwt.util';
import oidcUtil from './utils/oidc.util'; import oidcUtil from './utils/oidc.util';
import passkeyUtil from './utils/passkey.util'; import passkeyUtil from './utils/passkey.util';
@@ -117,7 +118,7 @@ test('End session without id token hint shows confirmation page', async ({ page
test('End session with id token hint redirects to callback URL', async ({ page }) => { test('End session with id token hint redirects to callback URL', async ({ page }) => {
const client = oidcClients.nextcloud; const client = oidcClients.nextcloud;
const idToken = idTokens.filter((token) => token.expired)[0].token; const idToken = await generateIdToken(users.tim, client.id);
let redirectedCorrectly = false; let redirectedCorrectly = false;
await page await page
.goto( .goto(
@@ -193,8 +194,8 @@ test('Using refresh token invalidates it for future use', async ({ request }) =>
test.describe('Introspection endpoint', () => { test.describe('Introspection endpoint', () => {
const client = oidcClients.nextcloud; const client = oidcClients.nextcloud;
const validAccessToken = accessTokens.filter((token) => !token.expired)[0].token;
test('without client_id and client_secret fails', async ({ request }) => { test('without client_id and client_secret fails', async ({ request }) => {
const validAccessToken = await generateOauthAccessToken(users.tim, client.id);
const introspectionResponse = await request.post('/api/oidc/introspect', { const introspectionResponse = await request.post('/api/oidc/introspect', {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
@@ -207,7 +208,8 @@ test.describe('Introspection endpoint', () => {
expect(introspectionResponse.status()).toBe(400); expect(introspectionResponse.status()).toBe(400);
}); });
test('with client_id and client_secret succeeds', async ({ request }) => { test('with client_id and client_secret succeeds', async ({ request, baseURL }) => {
const validAccessToken = await generateOauthAccessToken(users.tim, client.id);
const introspectionResponse = await request.post('/api/oidc/introspect', { const introspectionResponse = await request.post('/api/oidc/introspect', {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
@@ -222,7 +224,7 @@ test.describe('Introspection endpoint', () => {
const introspectionBody = await introspectionResponse.json(); const introspectionBody = await introspectionResponse.json();
expect(introspectionBody.active).toBe(true); expect(introspectionBody.active).toBe(true);
expect(introspectionBody.token_type).toBe('access_token'); expect(introspectionBody.token_type).toBe('access_token');
expect(introspectionBody.iss).toBe('http://localhost'); expect(introspectionBody.iss).toBe(baseURL);
expect(introspectionBody.sub).toBe(users.tim.id); expect(introspectionBody.sub).toBe(users.tim.id);
expect(introspectionBody.aud).toStrictEqual([oidcClients.nextcloud.id]); expect(introspectionBody.aud).toStrictEqual([oidcClients.nextcloud.id]);
}); });
@@ -265,7 +267,7 @@ test.describe('Introspection endpoint', () => {
}); });
test("expired access_token can't be verified", async ({ request }) => { test("expired access_token can't be verified", async ({ request }) => {
const expiredAccessToken = accessTokens.filter((token) => token.expired)[0].token; const expiredAccessToken = await generateOauthAccessToken(users.tim, client.id, true);
const introspectionResponse = await request.post('/api/oidc/introspect', { const introspectionResponse = await request.post('/api/oidc/introspect', {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'

View File

@@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import playwrightConfig from '../../playwright.config';
export async function cleanupBackend() { export async function cleanupBackend() {
await axios.post('http://localhost/api/test/reset'); await axios.post(playwrightConfig.use!.baseURL + '/api/test/reset');
} }

View File

@@ -0,0 +1,58 @@
import * as jose from 'jose';
import playwrightConfig from '../../playwright.config';
const PRIVATE_KEY_STRING = `{"alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"}`;
type User = {
id: string;
email: string;
firstname: string;
lastname: string;
};
const privateKey = JSON.parse(PRIVATE_KEY_STRING);
const privateKeyImported = await jose.importJWK(privateKey, 'RS256');
export async function generateIdToken(user: User, clientId: string, expired = false) {
const now = Math.floor(Date.now() / 1000);
const expiration = expired ? now + 1 : now + 1000000000; // Either expired or valid for a long time
const payload = {
aud: clientId,
email: user.email,
email_verified: true,
exp: expiration,
family_name: user.lastname,
given_name: user.firstname,
iat: now,
iss: playwrightConfig.use!.baseURL,
name: `${user.firstname} ${user.lastname}`,
nonce: 'oW1A1O78GQ15D73OsHEx7WQKj7ZqvHLZu_37mdXIqAQ',
sub: user.id,
type: 'id-token'
};
return await new jose.SignJWT(payload)
.setProtectedHeader({ alg: 'RS256', kid: privateKey.kid, typ: 'JWT' })
.sign(privateKeyImported);
}
export async function generateOauthAccessToken(user: User, clientId: string, expired = false) {
const now = Math.floor(Date.now() / 1000);
const expiration = expired ? now - 1000 : now + 1000000000; // Either expired or valid for a long time
const payload = {
aud: [clientId],
exp: expiration,
iat: now,
iss: playwrightConfig.use!.baseURL,
sub: user.id,
type: 'oauth-access-token'
};
return await new jose.SignJWT(payload)
.setProtectedHeader({ alg: 'RS256', kid: privateKey.kid, typ: 'JWT' })
.sign(privateKeyImported);
}

View File

@@ -13,5 +13,13 @@ export default defineConfig({
cookieName: 'locale', cookieName: 'locale',
strategy: ['cookie', 'preferredLanguage', 'baseLocale'] strategy: ['cookie', 'preferredLanguage', 'baseLocale']
}) })
] ],
server: {
proxy: {
'/api': {
target: process.env.DEVELOPMENT_BACKEND_URL || 'http://localhost:1411',
}
}
}
}); });

View File

@@ -1,5 +0,0 @@
:{$CADDY_PORT:80} {
reverse_proxy /api/* http://localhost:{$BACKEND_PORT:8080}
reverse_proxy /.well-known/* http://localhost:{$BACKEND_PORT:8080}
reverse_proxy /* http://localhost:{$PORT:3000}
}

View File

@@ -1,14 +0,0 @@
:{$CADDY_PORT:80} {
reverse_proxy /api/* http://localhost:{$BACKEND_PORT:8080} {
trusted_proxies 0.0.0.0/0
trusted_proxies ::/0
}
reverse_proxy /.well-known/* http://localhost:{$BACKEND_PORT:8080} {
trusted_proxies 0.0.0.0/0
trusted_proxies ::/0
}
reverse_proxy /* http://localhost:{$PORT:3000} {
trusted_proxies 0.0.0.0/0
trusted_proxies ::/0
}
}

View File

@@ -1,6 +1,7 @@
#!/bin/sh #!/bin/sh
DB_PATH="./backend/data/pocket-id.db" # TODO: Should parse DB_CONNECTION_STRING
DB_PATH="/app/data/pocket-id.db"
DB_PROVIDER="${DB_PROVIDER:=sqlite}" DB_PROVIDER="${DB_PROVIDER:=sqlite}"
# Parse command-line arguments for the -d flag (database path) # Parse command-line arguments for the -d flag (database path)
@@ -108,7 +109,7 @@ fi
echo "=================================================" echo "================================================="
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "A one-time access token valid for 1 hour has been created for \"$USER_IDENTIFIER\"." echo "A one-time access token valid for 1 hour has been created for \"$USER_IDENTIFIER\"."
echo "Use the following URL to sign in once: ${PUBLIC_APP_URL:=https://<your-pocket-id-domain>}/lc/$SECRET_TOKEN" echo "Use the following URL to sign in once: ${APP_URL:=https://<your-pocket-id-domain>}/lc/$SECRET_TOKEN"
else else
echo "Error creating access token." echo "Error creating access token."
exit 1 exit 1

View File

@@ -0,0 +1,57 @@
set -eu
cd backend
mkdir -p .bin
# Function to build for a specific platform
build_platform() {
target=$1
os=$2
arch=$3
arm_version=${4:-""}
pocket_id_version=$(cat ../.version)
# Set the binary extension to exe for Windows
binary_ext=""
if [ "$os" = "windows" ]; then
binary_ext=".exe"
fi
output_dir=".bin/pocket-id-${target}${binary_ext}"
printf "Building %s/%s%s" "$os" "$arch" "$([ -n "$arm_version" ] && echo " GOARM=$arm_version" || echo "")... "
# Build environment variables
env_vars="GOOS=${os} GOARCH=${arch}"
if [ -n "$arm_version" ]; then
env_vars="${env_vars} GOARM=${arm_version}"
fi
# Build the binary
eval "${env_vars} go build \
-ldflags='-X github.com/pocket-id/pocket-id/backend/internal/common.Version=${pocket_id_version}' \
-o \"${output_dir}\" \
-trimpath \
./cmd"
printf "Done\n"
}
# linux builds
build_platform "linux-amd64" "linux" "amd64" ""
build_platform "linux-386" "linux" "386" ""
build_platform "linux-arm64" "linux" "arm64" ""
build_platform "linux-armv7" "linux" "arm" "7"
# macOS builds
build_platform "macos-x64" "darwin" "amd64" ""
build_platform "macos-arm64" "darwin" "arm64" ""
# Windows builds
build_platform "windows-x64" "windows" "amd64" ""
build_platform "windows-arm64" "windows" "arm64" ""
# FreeBSD builds
build_platform "freebsd-amd64" "freebsd" "amd64" ""
build_platform "freebsd-arm64" "freebsd" "arm64" ""
echo "Compilation done"

View File

@@ -112,7 +112,7 @@ fi
# Create the release on GitHub # Create the release on GitHub
echo "Creating GitHub release..." echo "Creating GitHub release..."
gh release create "v$NEW_VERSION" --title "v$NEW_VERSION" --notes "$CHANGELOG" gh release create "v$NEW_VERSION" --title "v$NEW_VERSION" --notes "$CHANGELOG" --draft
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "GitHub release created successfully." echo "GitHub release created successfully."

View File

@@ -1,31 +0,0 @@
# If we aren't running as root, just exec the CMD
[ "$(id -u)" -ne 0 ] && exec "$@"
echo "Creating user and group..."
PUID=${PUID:-1000}
PGID=${PGID:-1000}
# Check if the group with PGID exists; if not, create it
if ! getent group pocket-id-group > /dev/null 2>&1; then
addgroup -g "$PGID" pocket-id-group
fi
# Check if a user with PUID exists; if not, create it
if ! id -u pocket-id > /dev/null 2>&1; then
if ! getent passwd "$PUID" > /dev/null 2>&1; then
adduser -u "$PUID" -G pocket-id-group pocket-id
else
# If a user with the PUID already exists, use that user
existing_user=$(getent passwd "$PUID" | cut -d: -f1)
echo "Using existing user: $existing_user"
fi
fi
# Change ownership of the /app directory
mkdir -p /app/backend/data
find /app/backend/data \( ! -group "${PGID}" -o ! -user "${PUID}" \) -exec chown "${PUID}:${PGID}" {} +
# Switch to the non-root user
exec su-exec "$PUID:$PGID" "$@"

View File

@@ -1,28 +1,38 @@
echo "Starting frontend..." #!/bin/sh
node frontend/build &
echo "Starting backend..." # Ensure we are in the /app folder
cd backend && ./pocket-id-backend & cd /app
if [ "$CADDY_DISABLED" != "true" ]; then # If we aren't running as root, just exec the CMD
echo "Starting Caddy..." if [ "$(id -u)" -ne 0 ] ; then
exec "$@"
# https://caddyserver.com/docs/conventions#data-directory exit 0
export XDG_DATA_HOME=${XDG_DATA_HOME:-/app/backend/data/.local/share}
# https://caddyserver.com/docs/conventions#configuration-directory
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-/app/backend/data/.config}
# Check if TRUST_PROXY is set to true and use the appropriate Caddyfile
if [ "$TRUST_PROXY" = "true" ]; then
caddy run --adapter caddyfile --config /etc/caddy/Caddyfile.trust-proxy &
else
caddy run --adapter caddyfile --config /etc/caddy/Caddyfile &
fi
else
echo "Caddy is disabled. Skipping..."
fi fi
# Set up trap to catch child process terminations PUID=${PUID:-1000}
trap 'exit 1' SIGCHLD PGID=${PGID:-1000}
wait # Check if the group with PGID exists; if not, create it
if ! getent group pocket-id-group > /dev/null 2>&1; then
echo "Creating group $PGID..."
addgroup -g "$PGID" pocket-id-group
fi
# Check if a user with PUID exists; if not, create it
if ! id -u pocket-id > /dev/null 2>&1; then
if ! getent passwd "$PUID" > /dev/null 2>&1; then
echo "Creating user $PUID..."
adduser -u "$PUID" -G pocket-id-group pocket-id > /dev/null 2>&1
else
# If a user with the PUID already exists, use that user
existing_user=$(getent passwd "$PUID" | cut -d: -f1)
echo "Using existing user: $existing_user"
fi
fi
# Change ownership of the /app/data directory
mkdir -p /app/data
find /app/data \( ! -group "${PGID}" -o ! -user "${PUID}" \) -exec chown "${PUID}:${PGID}" {} +
# Switch to the non-root user
exec su-exec "$PUID:$PGID" "$@"