Compare commits

...

22 Commits

Author SHA1 Message Date
Elias Schneider
a879bfa418 release: 0.27.1 2025-01-24 12:07:10 +01:00
Elias Schneider
164ce6a3d7 fix: add __HOST prefix to cookies (#175) 2025-01-24 12:01:27 +01:00
Chris Danis
ef1aeb7152 docs: make CONTRIBUTING instructions work & fix example envs (#152) 2025-01-24 10:51:26 +01:00
Elias Schneider
47c39f6d38 fix: use OS hostname for SMTP EHLO message 2025-01-24 10:36:16 +01:00
Elias Schneider
2884021055 chore: remove duplicate text from issue template 2025-01-23 19:57:50 +01:00
Kyle Mendell
def39b8703 chore: bug template update (#133)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-01-23 19:10:48 +01:00
Elias Schneider
d071641890 docs: remove duplicate contribute.md 2025-01-23 18:10:28 +01:00
Elias Schneider
397544c0f3 fix: send hostname derived from PUBLIC_APP_URL with SMTP EHLO command 2025-01-23 17:55:36 +01:00
Kyle Mendell
1fb99e5d52 docs: add more client-examples (#166) 2025-01-23 11:37:41 +01:00
Elias Schneider
7b403552ba chore: add GitHub release creation to create-release.sh script 2025-01-23 08:56:28 +01:00
Elias Schneider
440a9f1ba0 release: 0.27.0 2025-01-22 18:51:06 +01:00
Kyle Mendell
d02f4753f3 fix: add save changes dialog before sending test email (#165)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-01-22 18:49:25 +01:00
Kyle Mendell
ede7d8fc15 docs: fix open-webui docs page (#162) 2025-01-21 19:07:12 +01:00
imgbot[bot]
e4e6c9b680 refactor: optimize images (#161)
Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>
Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2025-01-21 18:59:16 +01:00
Kyle Mendell
c12bf2955b docs: add docusaurus docs (#118)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-01-21 18:46:42 +01:00
Elias Schneider
c211d3fc67 docs: add delay_start to caddy security 2025-01-20 17:54:38 +01:00
Kamil Kosek
d87eb416cd docs: create sample-configurations.md (#142)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
2025-01-20 11:54:35 +01:00
Giovanni
f7710f2988 fix: ensure the downloaded GeoLite2 DB is not corrupted & prevent RW race condition (#138) 2025-01-20 11:50:58 +01:00
Chris Danis
72923bb86d feat: display private IP ranges correctly in audit log (#139) 2025-01-20 11:36:12 +01:00
Elias Schneider
6e44b5e367 release: 0.26.0 2025-01-20 11:23:52 +01:00
Elias Schneider
8a1db0cb4a feat: support wildcard callback URLs 2025-01-20 11:19:23 +01:00
Elias Schneider
3f02d08109 fix: non LDAP users get created with a empty LDAP ID string 2025-01-20 10:41:01 +01:00
85 changed files with 19120 additions and 307 deletions

View File

@@ -34,4 +34,23 @@ body:
- type: markdown
attributes:
value: |
Before submitting, please check if the issues hasn't been raised before.
### Additional Information
- type: textarea
id: extra-information
validations:
required: true
attributes:
label: "Version and Environment"
description: "Please specify the version of Pocket ID, along with any environment-specific configurations, such your reverse proxy, that might be relevant."
placeholder: "e.g., v0.24.1"
- type: textarea
id: log-files
validations:
required: false
attributes:
label: "Log Output"
description: "Output of log files when the issue occured to help us diagnose the issue."
- type: markdown
attributes:
value: |
**Before submitting, please check if the issue hasn't been raised before.**

18
.gitignore vendored
View File

@@ -36,4 +36,20 @@ data
/frontend/tests/.auth
/frontend/tests/.report
pocket-id-backend
/backend/GeoLite2-City.mmdb
/backend/GeoLite2-City.mmdb
# Generated files
docs/build
docs/.docusaurus
docs/.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -1 +1 @@
0.25.1
0.27.1

View File

@@ -1,3 +1,37 @@
## [](https://github.com/stonith404/pocket-id/compare/v0.27.0...v) (2025-01-24)
### Bug Fixes
* add `__HOST` prefix to cookies ([#175](https://github.com/stonith404/pocket-id/issues/175)) ([164ce6a](https://github.com/stonith404/pocket-id/commit/164ce6a3d7fa8ae5275c94302952cf318e3b3113))
* send hostname derived from `PUBLIC_APP_URL` with SMTP EHLO command ([397544c](https://github.com/stonith404/pocket-id/commit/397544c0f3f2b49f1f34ae53e6b9daf194d1ae28))
* use OS hostname for SMTP EHLO message ([47c39f6](https://github.com/stonith404/pocket-id/commit/47c39f6d382c496cb964262adcf76cc8dbb96da3))
## [](https://github.com/stonith404/pocket-id/compare/v0.26.0...v) (2025-01-22)
### Features
* display private IP ranges correctly in audit log ([#139](https://github.com/stonith404/pocket-id/issues/139)) ([72923bb](https://github.com/stonith404/pocket-id/commit/72923bb86dc5d07d56aea98cf03320667944b553))
### Bug Fixes
* add save changes dialog before sending test email ([#165](https://github.com/stonith404/pocket-id/issues/165)) ([d02f475](https://github.com/stonith404/pocket-id/commit/d02f4753f3fbda75cd415ebbfe0702765c38c144))
* ensure the downloaded GeoLite2 DB is not corrupted & prevent RW race condition ([#138](https://github.com/stonith404/pocket-id/issues/138)) ([f7710f2](https://github.com/stonith404/pocket-id/commit/f7710f298898d322885c1c83680e26faaa0bb800))
## [](https://github.com/stonith404/pocket-id/compare/v0.25.1...v) (2025-01-20)
### Features
* support wildcard callback URLs ([8a1db0c](https://github.com/stonith404/pocket-id/commit/8a1db0cb4a5d4b32b4fdc19d41fff688a7c71a56))
### Bug Fixes
* non LDAP users get created with a empty LDAP ID string ([3f02d08](https://github.com/stonith404/pocket-id/commit/3f02d081098ad2caaa60a56eea4705639f80d01f))
## [](https://github.com/stonith404/pocket-id/compare/v0.25.0...v) (2025-01-19)

View File

@@ -55,14 +55,14 @@ The frontend is built with [SvelteKit](https://kit.svelte.dev) and written in Ty
3. Install the dependencies with `npm install`
4. Start the frontend with `npm run dev`
You're all set!
### Reverse Proxy
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!
### Testing
We are using [Playwright](https://playwright.dev) for end-to-end testing.

View File

@@ -21,7 +21,7 @@ RUN CGO_ENABLED=1 GOOS=linux go build -o /app/backend/pocket-id-backend .
# Stage 3: Production Image
FROM node:20-alpine
# Delete default node user
# Delete default node user
RUN deluser --remove-home node
RUN apk add --no-cache caddy curl su-exec

174
README.md
View File

@@ -10,181 +10,11 @@ The goal of Pocket ID is to be a simple and easy-to-use. There are other self-ho
Additionally, what makes Pocket ID special is that it only supports [passkey](https://www.passkeys.io/) authentication, which means you dont need a password. Some people might not like this idea at first, but I believe passkeys are the future, and once you try them, youll love them. For example, you can now use a physical Yubikey to sign in to all your self-hosted services easily and securely.
## Table of Contents
- [ Pocket ID](#-pocket-id)
- [Table of Contents](#table-of-contents)
- [Setup](#setup)
- [Before you start](#before-you-start)
- [Installation with Docker (recommended)](#installation-with-docker-recommended)
- [Unraid](#unraid)
- [Stand-alone Installation](#stand-alone-installation)
- [Nginx Reverse Proxy](#nginx-reverse-proxy)
- [Proxy Services with Pocket ID](#proxy-services-with-pocket-id)
- [Update](#update)
- [Docker](#docker)
- [Stand-alone](#stand-alone)
- [Environment variables](#environment-variables)
- [Account recovery](#account-recovery)
- [Contribute](#contribute)
## Setup
> [!WARNING]
> Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue.
Pocket ID can be set up in multiple ways. The easiest and recommended way is to use Docker.
### Before you start
Pocket ID requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), meaning it must be served over HTTPS. This is necessary because Pocket ID uses the [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).
### Installation with Docker (recommended)
1. Download the `docker-compose.yml` and `.env` file:
```bash
curl -O https://raw.githubusercontent.com/stonith404/pocket-id/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/stonith404/pocket-id/main/.env.example
```
2. Edit the `.env` file so that it fits your needs. See the [environment variables](#environment-variables) section for more information.
3. Run `docker compose up -d`
You can now sign in with the admin account on `http://localhost/login/setup`.
### Unraid
Pocket ID is available as a template on the Community Apps store.
### Stand-alone Installation
Required tools:
- [Node.js](https://nodejs.org/en/download/) >= 20
- [Go](https://golang.org/doc/install) >= 1.23
- [Git](https://git-scm.com/downloads)
- [PM2](https://pm2.keymetrics.io/)
- [Caddy](https://caddyserver.com/docs/install) (optional)
1. Copy the `.env.example` file in the `frontend` and `backend` folder to `.env` and change it so that it fits your needs.
```bash
cp frontend/.env.example frontend/.env
cp backend/.env.example backend/.env
```
2. Run the following commands:
```bash
git clone https://github.com/stonith404/pocket-id
cd pocket-id
# Checkout the latest version
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
# Start the backend
cd backend/cmd
go build -o ../pocket-id-backend
cd ..
pm2 start pocket-id-backend --name pocket-id-backend
# Start the frontend
cd ../frontend
npm install
npm run build
pm2 start --name pocket-id-frontend --node-args="--env-file .env" build/index.js
# Optional: Start Caddy (You can use any other reverse proxy)
cd ..
pm2 start --name pocket-id-caddy caddy -- run --config reverse-proxy/Caddyfile
```
You can now sign in with the admin account on `http://localhost/login/setup`.
### Nginx Reverse Proxy
To use Nginx as a reverse proxy for Pocket ID, update the configuration to increase the header buffer size. This adjustment is necessary because SvelteKit generates larger headers, which may exceed the default buffer limits.
```nginx
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
```
## Proxy Services with Pocket ID
The goal of Pocket ID is to function exclusively as an OIDC provider. As such, we don't have a built-in proxy provider. However, you can use other tools that act as a middleware to protect your services and support OIDC as an authentication provider.
See the [guide](docs/proxy-services.md) for more information.
## Update
#### Docker
```bash
docker compose pull
docker compose up -d
```
#### Stand-alone
1. Stop the running services:
```bash
pm2 delete pocket-id-backend pocket-id-frontend pocket-id-caddy
```
2. Run the following commands:
```bash
cd pocket-id
# Checkout the latest version
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
# Start the backend
cd backend/cmd
go build -o ../pocket-id-backend
cd ..
pm2 start pocket-id-backend --name pocket-id-backend
# Start the frontend
cd ../frontend
npm install
npm run build
pm2 start build/index.js --name pocket-id-frontend
# Optional: Start Caddy (You can use any other reverse proxy)
cd ..
pm2 start caddy --name pocket-id-caddy -- run --config reverse-proxy/Caddyfile
```
## Environment variables
| Variable | Default Value | Recommended to change | Description |
| ---------------------------- | ------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. |
| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. |
| `MAXMIND_LICENSE_KEY` | `-` | yes | License Key for the GeoLite2 Database. The license key is required to retrieve the geographical location of IP addresses in the audit log. If the key is not provided, IP locations will be marked as "unknown." You can obtain a license key for free [here](https://www.maxmind.com/en/geolite2/signup). |
| `PUID` and `PGID` | `1000` | yes | The user and group ID of the user who should run Pocket ID inside the Docker container and owns the files that are mounted with the volume. You can get the `PUID` and `GUID` of your user on your host machine by using the command `id`. For more information see [this article](https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables). |
| `DB_PROVIDER` | `sqlite` | no | The database provider you want to use. Currently `sqlite` and `postgres` are supported. |
| `SQLITE_DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. This gets ignored if you didn't set `DB_PROVIDER` to `sqlite`. |
| `POSTGRES_CONNECTION_STRING` | `-` | no | The connection string to your Postgres database. This gets ignored if you didn't set `DB_PROVIDER` to `postgres`. A connection string can look like this: `postgresql://user:password@host:5432/pocket-id`. |
| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. |
| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. |
| `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. |
| `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. |
| `PORT` | `3000` | no | The port on which the frontend should listen. |
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen. |
## Account recovery
There are two ways to create a one-time access link for a user:
1. **UI**: An admin can create a one-time access link for the user in the admin panel under the "Users" tab by clicking on the three dots next to the user's name and selecting "One-time link".
2. **Terminal**: You can create a one-time access link for a user by running the `scripts/create-one-time-access-token.sh` script. To execute this script with Docker you have to run the following command:
```bash
docker compose exec pocket-id sh "sh scripts/create-one-time-access-token.sh <username or email>"
```
Visit the [documentation](https://stonith404.github.io/pocket-id) for the setup guide and more information.
## Contribute

View File

@@ -1,6 +1,8 @@
APP_ENV=production
PUBLIC_APP_URL=http://localhost
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
DB_PROVIDER=sqlite
# MAXMIND_LICENSE_KEY=fixme # needed for IP geolocation in the audit log
SQLITE_DB_PATH=data/pocket-id.db
POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@localhost:5432/pocket-id
UPLOAD_PATH=data/uploads

View File

@@ -6,9 +6,10 @@ import (
)
func Bootstrap() {
initApplicationImages()
db := newDatabase()
appConfigService := service.NewAppConfigService(db)
initApplicationImages()
initRouter(db, appConfigService)
}

View File

@@ -1,7 +1,9 @@
package controller
import (
"github.com/stonith404/pocket-id/backend/internal/utils/cookie"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -184,7 +186,10 @@ func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) {
return
}
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
sessionDurationInMinutesParsed, _ := strconv.Atoi(uc.appConfigService.DbConfig.SessionDuration.Value)
maxAge := sessionDurationInMinutesParsed * 60
cookie.AddAccessTokenCookie(c, maxAge, token)
c.JSON(http.StatusOK, userDto)
}
@@ -201,7 +206,10 @@ func (uc *UserController) getSetupAccessTokenHandler(c *gin.Context) {
return
}
utils.AddAccessTokenCookie(c, uc.appConfigService.DbConfig.SessionDuration.Value, token)
sessionDurationInMinutesParsed, _ := strconv.Atoi(uc.appConfigService.DbConfig.SessionDuration.Value)
maxAge := sessionDurationInMinutesParsed * 60
cookie.AddAccessTokenCookie(c, maxAge, token)
c.JSON(http.StatusOK, userDto)
}

View File

@@ -5,8 +5,9 @@ import (
"github.com/stonith404/pocket-id/backend/internal/common"
"github.com/stonith404/pocket-id/backend/internal/dto"
"github.com/stonith404/pocket-id/backend/internal/middleware"
"github.com/stonith404/pocket-id/backend/internal/utils"
"github.com/stonith404/pocket-id/backend/internal/utils/cookie"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -42,12 +43,12 @@ func (wc *WebauthnController) beginRegistrationHandler(c *gin.Context) {
return
}
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
cookie.AddSessionIdCookie(c, int(options.Timeout.Seconds()), options.SessionID)
c.JSON(http.StatusOK, options.Response)
}
func (wc *WebauthnController) verifyRegistrationHandler(c *gin.Context) {
sessionID, err := c.Cookie("session_id")
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
if err != nil {
c.Error(&common.MissingSessionIdError{})
return
@@ -76,12 +77,12 @@ func (wc *WebauthnController) beginLoginHandler(c *gin.Context) {
return
}
c.SetCookie("session_id", options.SessionID, int(options.Timeout.Seconds()), "/", "", true, true)
cookie.AddSessionIdCookie(c, int(options.Timeout.Seconds()), options.SessionID)
c.JSON(http.StatusOK, options.Response)
}
func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
sessionID, err := c.Cookie("session_id")
sessionID, err := c.Cookie(cookie.SessionIdCookieName)
if err != nil {
c.Error(&common.MissingSessionIdError{})
return
@@ -105,7 +106,10 @@ func (wc *WebauthnController) verifyLoginHandler(c *gin.Context) {
return
}
utils.AddAccessTokenCookie(c, wc.appConfigService.DbConfig.SessionDuration.Value, token)
sessionDurationInMinutesParsed, _ := strconv.Atoi(wc.appConfigService.DbConfig.SessionDuration.Value)
maxAge := sessionDurationInMinutesParsed * 60
cookie.AddAccessTokenCookie(c, maxAge, token)
c.JSON(http.StatusOK, userDto)
}
@@ -165,6 +169,6 @@ func (wc *WebauthnController) updateCredentialHandler(c *gin.Context) {
}
func (wc *WebauthnController) logoutHandler(c *gin.Context) {
utils.AddAccessTokenCookie(c, "0", "")
cookie.AddAccessTokenCookie(c, 0, "")
c.Status(http.StatusNoContent)
}

View File

@@ -37,6 +37,6 @@ type AppConfigUpdateDto struct {
LdapAttributeGroupUniqueIdentifier string `json:"ldapAttributeGroupUniqueIdentifier"`
LdapAttributeGroupName string `json:"ldapAttributeGroupName"`
LdapAttributeAdminGroup string `json:"ldapAttributeAdminGroup"`
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
EmailOneTimeAccessEnabled string `json:"emailOneTimeAccessEnabled" binding:"required"`
EmailLoginNotificationEnabled string `json:"emailLoginNotificationEnabled" binding:"required"`
}

View File

@@ -16,7 +16,7 @@ type OidcClientDto struct {
type OidcClientCreateDto struct {
Name string `json:"name" binding:"required,max=50"`
CallbackURLs []string `json:"callbackURLs" binding:"required,urlList"`
CallbackURLs []string `json:"callbackURLs" binding:"required"`
IsPublic bool `json:"isPublic"`
PkceEnabled bool `json:"pkceEnabled"`
}

View File

@@ -4,21 +4,9 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
"log"
"net/url"
"regexp"
)
var validateUrlList validator.Func = func(fl validator.FieldLevel) bool {
urls := fl.Field().Interface().([]string)
for _, u := range urls {
_, err := url.ParseRequestURI(u)
if err != nil {
return false
}
}
return true
}
var validateUsername validator.Func = func(fl validator.FieldLevel) bool {
// [a-zA-Z0-9] : The username must start with an alphanumeric character
// [a-zA-Z0-9_.@-]* : The rest of the username can contain alphanumeric characters, dots, underscores, hyphens, and "@" symbols
@@ -36,11 +24,6 @@ var validateClaimKey validator.Func = func(fl validator.FieldLevel) bool {
}
func init() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
if err := v.RegisterValidation("urlList", validateUrlList); err != nil {
log.Fatalf("Failed to register custom validation: %v", err)
}
}
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
if err := v.RegisterValidation("username", validateUsername); err != nil {
log.Fatalf("Failed to register custom validation: %v", err)

View File

@@ -83,8 +83,6 @@ func handleValidationError(validationErrors validator.ValidationErrors) string {
errorMessage = fmt.Sprintf("%s must be at least %s characters long", fieldName, ve.Param())
case "max":
errorMessage = fmt.Sprintf("%s must be at most %s characters long", fieldName, ve.Param())
case "urlList":
errorMessage = fmt.Sprintf("%s must be a list of valid URLs", fieldName)
default:
errorMessage = fmt.Sprintf("%s is invalid", fieldName)
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/stonith404/pocket-id/backend/internal/common"
"github.com/stonith404/pocket-id/backend/internal/service"
"github.com/stonith404/pocket-id/backend/internal/utils/cookie"
"strings"
)
@@ -19,7 +20,7 @@ func NewJwtAuthMiddleware(jwtService *service.JwtService, ignoreUnauthenticated
func (m *JwtAuthMiddleware) Add(adminOnly bool) gin.HandlerFunc {
return func(c *gin.Context) {
// Extract the token from the cookie or the Authorization header
token, err := c.Cookie("access_token")
token, err := c.Cookie(cookie.AccessTokenCookieName)
if err != nil {
authorizationHeaderSplitted := strings.Split(c.GetHeader("Authorization"), " ")
if len(authorizationHeaderSplitted) == 2 {

View File

@@ -20,14 +20,14 @@ type AppConfig struct {
LogoLightImageType AppConfigVariable
LogoDarkImageType AppConfigVariable
// Email
SmtpHost AppConfigVariable
SmtpPort AppConfigVariable
SmtpFrom AppConfigVariable
SmtpUser AppConfigVariable
SmtpPassword AppConfigVariable
SmtpTls AppConfigVariable
SmtpSkipCertVerify AppConfigVariable
EmailLoginNotificationEnabled AppConfigVariable
SmtpHost AppConfigVariable
SmtpPort AppConfigVariable
SmtpFrom AppConfigVariable
SmtpUser AppConfigVariable
SmtpPassword AppConfigVariable
SmtpTls AppConfigVariable
SmtpSkipCertVerify AppConfigVariable
EmailLoginNotificationEnabled AppConfigVariable
EmailOneTimeAccessEnabled AppConfigVariable
// LDAP
LdapEnabled AppConfigVariable

View File

@@ -73,7 +73,7 @@ var defaultDbConfig = model.AppConfig{
IsInternal: true,
DefaultValue: "svg",
},
// Email
// Email
SmtpHost: model.AppConfigVariable{
Key: "smtpHost",
Type: "string",
@@ -104,7 +104,7 @@ var defaultDbConfig = model.AppConfig{
Type: "bool",
DefaultValue: "false",
},
EmailLoginNotificationEnabled: model.AppConfigVariable{
EmailLoginNotificationEnabled: model.AppConfigVariable{
Key: "emailLoginNotificationEnabled",
Type: "bool",
DefaultValue: "false",

View File

@@ -14,6 +14,7 @@ import (
"net"
"net/smtp"
"net/textproto"
"os"
ttemplate "text/template"
"time"
)
@@ -99,7 +100,7 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
smtpAddress := srv.appConfigService.DbConfig.SmtpHost.Value + ":" + port
var client *smtp.Client
if srv.appConfigService.DbConfig.SmtpTls.Value == "false" {
client, err = smtp.Dial(smtpAddress)
client, err = srv.connectToSmtpServer(smtpAddress)
} else if port == "465" {
client, err = srv.connectToSmtpServerUsingImplicitTLS(
smtpAddress,
@@ -115,9 +116,16 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %w", err)
}
defer client.Close()
// Set the hello message manually as for example Google rejects the default "localhost" value
hostname, err := os.Hostname()
if err == nil {
if err := client.Hello(hostname); err != nil {
return fmt.Errorf("failed to say hello to SMTP server: %w", err)
}
}
smtpUser := srv.appConfigService.DbConfig.SmtpUser.Value
smtpPassword := srv.appConfigService.DbConfig.SmtpPassword.Value
@@ -141,6 +149,15 @@ func SendEmail[V any](srv *EmailService, toEmail email.Address, template email.T
return nil
}
func (srv *EmailService) connectToSmtpServer(serverAddr string) (*smtp.Client, error) {
conn, err := netDialer.Dial("tcp", serverAddr)
if err != nil {
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
}
client, err := smtp.NewClient(conn, srv.appConfigService.DbConfig.SmtpHost.Value)
return client, err
}
func (srv *EmailService) connectToSmtpServerUsingImplicitTLS(serverAddr string, tlsConfig *tls.Config) (*smtp.Client, error) {
tlsDialer := &tls.Dialer{
NetDialer: netDialer,

View File

@@ -12,6 +12,7 @@ import (
"net/netip"
"os"
"path/filepath"
"sync"
"time"
"github.com/oschwald/maxminddb-golang/v2"
@@ -19,7 +20,24 @@ import (
"github.com/stonith404/pocket-id/backend/internal/common"
)
type GeoLiteService struct{}
type GeoLiteService struct {
mutex sync.Mutex
}
var localhostIPNets = []*net.IPNet{
{IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 127.0.0.0/8
{IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, // ::1/128
}
var privateLanIPNets = []*net.IPNet{
{IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32)}, // 10.0.0.0/8
{IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32)}, // 172.16.0.0/12
{IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32)}, // 192.168.0.0/16
}
var tailscaleIPNets = []*net.IPNet{
{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)}, // 100.64.0.0/10
}
// NewGeoLiteService initializes a new GeoLiteService instance and starts a goroutine to update the GeoLite2 City database.
func NewGeoLiteService() *GeoLiteService {
@@ -36,13 +54,29 @@ func NewGeoLiteService() *GeoLiteService {
// GetLocationByIP returns the country and city of the given IP address.
func (s *GeoLiteService) GetLocationByIP(ipAddress string) (country, city string, err error) {
// Check if IP is in Tailscale's CGNAT range (100.64.0.0/10)
// Check the IP address against known private IP ranges
if ip := net.ParseIP(ipAddress); ip != nil {
if ip.To4() != nil && ip.To4()[0] == 100 && ip.To4()[1] >= 64 && ip.To4()[1] <= 127 {
return "Internal Network", "Tailscale", nil
for _, ipNet := range tailscaleIPNets {
if ipNet.Contains(ip) {
return "Internal Network", "Tailscale", nil
}
}
for _, ipNet := range privateLanIPNets {
if ipNet.Contains(ip) {
return "Internal Network", "LAN/Docker/k8s", nil
}
}
for _, ipNet := range localhostIPNets {
if ipNet.Contains(ip) {
return "Internal Network", "localhost", nil
}
}
}
// Race condition between reading and writing the database.
s.mutex.Lock()
defer s.mutex.Unlock()
db, err := maxminddb.Open(common.EnvConfig.GeoLiteDBPath)
if err != nil {
return "", "", err
@@ -134,16 +168,44 @@ func (s *GeoLiteService) extractDatabase(reader io.Reader) error {
// Check if the file is the GeoLite2-City.mmdb file
if header.Typeflag == tar.TypeReg && filepath.Base(header.Name) == "GeoLite2-City.mmdb" {
outFile, err := os.Create(common.EnvConfig.GeoLiteDBPath)
// extract to a temporary file to avoid having a corrupted db in case of write failure.
baseDir := filepath.Dir(common.EnvConfig.GeoLiteDBPath)
tmpFile, err := os.CreateTemp(baseDir, "geolite.*.mmdb.tmp")
if err != nil {
return fmt.Errorf("failed to create target database file: %w", err)
return fmt.Errorf("failed to create temporary database file: %w", err)
}
defer outFile.Close()
tempName := tmpFile.Name()
// Write the file contents directly to the target location
if _, err := io.Copy(outFile, tarReader); err != nil {
if _, err := io.Copy(tmpFile, tarReader); err != nil {
// if fails to write, then cleanup and throw an error
tmpFile.Close()
os.Remove(tempName)
return fmt.Errorf("failed to write database file: %w", err)
}
tmpFile.Close()
// ensure the database is not corrupted
db, err := maxminddb.Open(tempName)
if err != nil {
// if fails to write, then cleanup and throw an error
os.Remove(tempName)
return fmt.Errorf("failed to open downloaded database file: %w", err)
}
db.Close()
// ensure we lock the structure before we overwrite the database
// to prevent race conditions between reading and writing the mmdb.
s.mutex.Lock()
// replace the old file with the new file
err = os.Rename(tempName, common.EnvConfig.GeoLiteDBPath)
s.mutex.Unlock()
if err != nil {
// if cannot overwrite via rename, then cleanup and throw an error
os.Remove(tempName)
return fmt.Errorf("failed to replace database file: %w", err)
}
return nil
}
}

View File

@@ -14,7 +14,7 @@ import (
"gorm.io/gorm"
"mime/multipart"
"os"
"slices"
"regexp"
"strings"
"time"
)
@@ -432,8 +432,16 @@ func (s *OidcService) getCallbackURL(client model.OidcClient, inputCallbackURL s
if inputCallbackURL == "" {
return client.CallbackURLs[0], nil
}
if slices.Contains(client.CallbackURLs, inputCallbackURL) {
return inputCallbackURL, nil
for _, callbackPattern := range client.CallbackURLs {
regexPattern := strings.ReplaceAll(regexp.QuoteMeta(callbackPattern), `\*`, ".*") + "$"
matched, err := regexp.MatchString(regexPattern, inputCallbackURL)
if err != nil {
return "", err
}
if matched {
return inputCallbackURL, nil
}
}
return "", &common.OidcInvalidCallbackURLError{}

View File

@@ -62,7 +62,10 @@ func (s *UserGroupService) Create(input dto.UserGroupCreateDto) (group model.Use
group = model.UserGroup{
FriendlyName: input.FriendlyName,
Name: input.Name,
LdapID: &input.LdapID,
}
if input.LdapID != "" {
group.LdapID = &input.LdapID
}
if err := s.db.Preload("Users").Create(&group).Error; err != nil {

View File

@@ -66,8 +66,11 @@ func (s *UserService) CreateUser(input dto.UserCreateDto) (model.User, error) {
Email: input.Email,
Username: input.Username,
IsAdmin: input.IsAdmin,
LdapID: &input.LdapID,
}
if input.LdapID != "" {
user.LdapID = &input.LdapID
}
if err := s.db.Create(&user).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return model.User{}, s.checkDuplicatedFields(user)

View File

@@ -0,0 +1,13 @@
package cookie
import (
"github.com/gin-gonic/gin"
)
func AddAccessTokenCookie(c *gin.Context, maxAgeInSeconds int, token string) {
c.SetCookie(AccessTokenCookieName, token, maxAgeInSeconds, "/", "", true, true)
}
func AddSessionIdCookie(c *gin.Context, maxAgeInSeconds int, sessionID string) {
c.SetCookie(SessionIdCookieName, sessionID, maxAgeInSeconds, "/", "", true, true)
}

View File

@@ -0,0 +1,16 @@
package cookie
import (
"github.com/stonith404/pocket-id/backend/internal/common"
"strings"
)
var AccessTokenCookieName = "__Host-access_token"
var SessionIdCookieName = "__Host-session"
func init() {
if strings.HasPrefix(common.EnvConfig.AppURL, "http://") {
AccessTokenCookieName = "access_token"
SessionIdCookieName = "session"
}
}

View File

@@ -1,12 +0,0 @@
package utils
import (
"github.com/gin-gonic/gin"
"strconv"
)
func AddAccessTokenCookie(c *gin.Context, sessionDurationInMinutes string, token string) {
sessionDurationInMinutesParsed, _ := strconv.Atoi(sessionDurationInMinutes)
maxAge := sessionDurationInMinutesParsed * 60
c.SetCookie("access_token", token, maxAge, "/", "", true, true)
}

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015">
<path fill="white" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015"><path fill="#fff" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/></svg>

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015">
<path fill="black" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015"><path fill="#000" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/></svg>

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 427 B

View File

@@ -0,0 +1,2 @@
UPDATE users SET ldap_id = '' WHERE ldap_id IS NULL;
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;

View File

@@ -0,0 +1,2 @@
UPDATE users SET ldap_id = null WHERE ldap_id = '';
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';

View File

@@ -0,0 +1,2 @@
UPDATE users SET ldap_id = '' WHERE ldap_id IS NULL;
UPDATE user_groups SET ldap_id = '' WHERE ldap_id IS NULL;

View File

@@ -0,0 +1,2 @@
UPDATE users SET ldap_id = null WHERE ldap_id = '';
UPDATE user_groups SET ldap_id = null WHERE ldap_id = '';

View File

@@ -0,0 +1,26 @@
---
id: cloudflare-zero-trust
---
# Cloudflare Zero Trust
**Note: Cloudflare will need to be able to reach your Pocket ID instance and vice versa for this to work correctly**
## Pocket ID Setup
1. In Pocket-ID create a new OIDC Client, name it i.e. `Cloudflare Zero Trust`.
2. Set a logo for this OIDC Client if you would like too.
3. Set the callback URL to: `https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback`.
4. Copy the Client ID, Client Secret, Authorization URL, Token URL, and Certificate URL for the next steps.
## Cloudflare Zero Trust Setup
1. Login to Cloudflare Zero Trust [Dashboard](https://one.dash.cloudflare.com/).
2. Navigate to Settings > Authentication > Login Methods.
3. Click `Add New` under login methods.
4. Create a name for the new login method.
5. Paste in the `Client ID` from Pocket ID into the `App ID` field.
6. Paste the `Client Secret` from Pocket ID into the `Client Secret` field.
7. Paste the `Authorization URL` from Pocket ID into the `Auth URL` field.
8. Paste the `Token URL` from Pocket ID into the `Token URL` field.
9. Paste the `Certificate URL` from Pocket ID into the `Certificate URL` field.
10. Save the new login method and test to make sure it works with cloudflare.

View File

@@ -0,0 +1,22 @@
---
id: grist
---
# Grist
## Pocket ID Setup
1. In Pocket-ID create a new OIDC Client, name it i.e. `Grist`
2. Set the callback url to: `https://<Grist Host>/oauth2/callback`
3. In Grist (Docker/Docker Compose/etc), set these environment variables:
```ini
GRIST_OIDC_IDP_ISSUER="https://<Pocket ID Host>/.well-known/openid-configuration"
GRIST_OIDC_IDP_CLIENT_ID="<Client ID from the OIDC Client created in Pocket ID>"
GRIST_OIDC_IDP_CLIENT_SECRET="<Client Secret from the OIDC Client created in Pocket ID>"
GRIST_OIDC_SP_HOST="https://<Grist Host>"
GRIST_OIDC_IDP_SCOPES="openid email profile" # Default
GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true # Default=false, needs to be true for Pocket Id b/c end_session_endpoint is not implemented
GRIST_OIDC_IDP_END_SESSION_ENDPOINT="https://<Pocket ID Host>/api/webauthn/logout" # Only set this if GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=false and you need to define a custom endpoint
```
4. Also ensure that the `GRIST_DEFAULT_EMAIL` env variable is set to the same email address as your user profile within Pocket ID
5. Start/Restart Grist

View File

@@ -0,0 +1,25 @@
---
id: hoarder
---
# Hoarder
1. In Pocket-ID create a new OIDC Client, name it i.e. `Hoarder`
2. Set the callback url to: `https://<your-hoarder-subdomain>.<your-domain>/api/auth/callback/custom`
3. Open your `.env` file from your Hoarder compose and add these lines:
```ini
OAUTH_WELLKNOWN_URL = https://<your-pocket-id-subdomain>.<your-domain>/.well-known/openid-configuration
OAUTH_CLIENT_SECRET = <client secret from the created OIDC client>
OAUTH_CLIENT_ID = <client id from the created OIDC client>
OAUTH_PROVIDER_NAME = Pocket-Id
NEXTAUTH_URL = https:///<your-hoarder-subdomain>.<your-domain>
```
4. Optional: If you like to disable password authentication and link your existing hoarder account with your pocket-id identity
```ini
DISABLE_PASSWORD_AUTH = true
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING = true
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -1,15 +1,21 @@
# Jellyfin SSO Integration Guide
---
id: jellyfin
---
# Jellyfin
> Due to the current limitations of the Jellyfin SSO plugin, this integration will only work in a browser. When tested, the Jellyfin app did not work and displayed an error, even when custom menu buttons were created.
> To view the original references and a full list of capabilities, please visit the [Jellyfin SSO OpenID Section](https://github.com/9p4/jellyfin-plugin-sso?tab=readme-ov-file#openid).
### Requirements
## Requirements
- [Jellyfin Server](https://jellyfin.org/downloads/server)
- [Jellyfin SSO Plugin](https://github.com/9p4/jellyfin-plugin-sso)
- HTTPS connection to your Jellyfin server
### OIDC - Pocket ID Setup
## OIDC - Pocket ID Setup
To start, we need to create a new SSO resource in our Jellyfin application.
> Replace the `JELLYFINDOMAIN` and `PROVIDER` elements in the URL.
@@ -20,32 +26,34 @@ To start, we need to create a new SSO resource in our Jellyfin application.
4. For this example, well be using the provider named "test_resource."
5. Click **Save**. Keep the page open, as we will need the OID client ID and OID secret.
### OIDC Client - Jellyfin SSO Resource
## OIDC Client - Jellyfin SSO Resource
1. Visit the plugin page (<i>Administration Dashboard -> My Plugins -> SSO-Auth</i>).
2. Enter the <i>OID Provider Name (we used "test_resource" as our name in the callback URL), Open ID, OID Secret, and mark it as enabled.</i>
3. The following steps are optional based on your needs. In this guide, well be managing only regular users, not admins.
![img.png](imgs/jelly_fin_img.png)
![img.png](imgs/jellyfin_img.png)
> To manage user access through groups, follow steps **4, 5, and 6**. Otherwise, leave it blank and skip to step 7.
![img2.png](imgs/jelly_fin_img2.png)
![img2.png](imgs/jellyfin_img2.png)
4. Under <i>Roles</i>, type the name of the group you want to use. **Note:** This must be the group name, not the label. Double-check in Pocket ID, as an incorrect name will lock users out.
5. Skip every field until you reach the **Role Claim** field, and type `groups`.
> This step is crucial if you want to manage users through groups.
6. Repeat the above step under **Request Additional Scopes**. This will pull the group scope during the sign-in process; otherwise, the previous steps wont work.
![img3.png](imgs/jelly_fin_img3.png)
![img3.png](imgs/jellyfin_img3.png)
7. Skip the remaining fields until you reach **Scheme Override**. Enter `https` here. If omitted, it will attempt to use HTTP first, which will break as WebAuthn requires an HTTPS connection.
8. Click **Save** and restart Jellyfin.
### Optional Step - Custom Home Button
## Optional Step - Custom Home Button
Follow the [guide to create a login button on the login page](https://github.com/9p4/jellyfin-plugin-sso?tab=readme-ov-file#creating-a-login-button-on-the-main-page) to add a custom button on your sign-in page. This step is optional, as you could also provide the sign-in URL via a bookmark or other means.
### Signing into Your Jellyfin Instance
## Signing into Your Jellyfin Instance
Done! You have successfully set up SSO for your Jellyfin instance using Pocket ID.
> **Note:** Sometimes there may be a brief delay when using the custom menu option. This is related to the Jellyfin plugin and not Pocket ID.

View File

@@ -0,0 +1,46 @@
---
id: netbox
---
# Netbox
**This guide does not currently show how to map groups in netbox from OIDC claims**
The following example variables are used, and should be replaced with your actual URLS.
- netbox.example.com (The url of your netbox instance.)
- id.example.com (The url of your Pocket ID instance.)
## Pocket ID Setup
1. In Pocket-ID create a new OIDC Client, name it i.e. `Netbox`.
2. Set a logo for this OIDC Client if you would like too.
3. Set the callback URL to: `https://netbox.example.com/oauth/complete/oidc/`.
4. Copy the `Client ID`, and the `Client Secret` for use in the next steps.
## Netbox Setup
This guide assumes you are using the git based install of netbox.
1. On your netbox server navigate to `/opt/netbox/netbox/netbox`
2. Add the following to your `configuration.py` file:
```python
# Remote authentication support
REMOTE_AUTH_ENABLED = True
REMOTE_AUTH_BACKEND = 'social_core.backends.open_id_connect.OpenIdConnectAuth'
REMOTE_AUTH_HEADER = 'HTTP_REMOTE_USER'
REMOTE_AUTH_USER_FIRST_NAME = 'HTTP_REMOTE_USER_FIRST_NAME'
REMOTE_AUTH_USER_LAST_NAME = 'HTTP_REMOTE_USER_LAST_NAME'
REMOTE_AUTH_USER_EMAIL = 'HTTP_REMOTE_USER_EMAIL'
REMOTE_AUTH_AUTO_CREATE_USER = True
REMOTE_AUTH_DEFAULT_GROUPS = []
REMOTE_AUTH_DEFAULT_PERMISSIONS = {}
SOCIAL_AUTH_OIDC_ENDPOINT = 'https://id.example.com'
SOCIAL_AUTH_OIDC_KEY = '<client id from the first part of this guide>'
SOCIAL_AUTH_OIDC_SECRET = '<client id from the first part of this guide>'
LOGOUT_REDIRECT_URL = 'https://netbox.example.com'
```
3. Save the file and restart netbox: `sudo systemctl start netbox netbox-rq`

View File

@@ -0,0 +1,17 @@
---
id: open-webui
---
# Open WebUI
1. In Pocket-ID, create a new OIDC Client, name it i.e. `Open WebUI`.
2. Set the callback URL to: `https://openwebui.domain/oauth/oidc/callback`
3. Add the following to your docker `.env` file for Open WebUI:
```ini
ENABLE_OAUTH_SIGNUP=true
OAUTH_CLIENT_ID=<client id from pocket ID>
OAUTH_CLIENT_SECRET=<client secret from pocket ID>
OAUTH_PROVIDER_NAME=Pocket ID
OPENID_PROVIDER_URL=https://<your pocket id url>/.well-known/openid-configuration
```

View File

@@ -0,0 +1,42 @@
---
id: pgadmin
---
# pgAdmin
The following example variables are used, and should be replaced with your actual URLS.
- pgadmin.example.com (The url of your pgAdmin instance.)
- id.example.com (The url of your Pocket ID instance.)
## Pocket ID Setup
1. In Pocket-ID create a new OIDC Client, name it i.e. `pgAdmin`.
2. Set a logo for this OIDC Client if you would like too.
3. Set the callback URL to: `https://pgadmin.example.com/oauth2/authorize`.
4. Copy the `Client ID`, `Client Secret`, `Authorization URL`, `Userinfo URL`, `Token URL`, and `OIDC Discovery URL` for use in the next steps.
# pgAdmin Setup
1. Add the following to the `config_local.py` file for pgAdmin:
**Make sure to replace https://id.example.com with your actual Pocket ID URL**
```python
AUTHENTICATION_SOURCES = ['oauth2', 'internal'] # This keeps internal authentication enabled as well as oauth2
OAUTH2_AUTO_CREATE_USER = True
OAUTH2_CONFIG = [{
'OAUTH2_NAME' : 'pocketid',
'OAUTH2_DISPLAY_NAME' : 'Pocket ID',
'OAUTH2_CLIENT_ID' : '<client id from the earlier step>',
'OAUTH2_CLIENT_SECRET' : '<client secret from the earlier step>',
'OAUTH2_TOKEN_URL' : 'https://id.example.com/api/oidc/token',
'OAUTH2_AUTHORIZATION_URL' : 'https://id.example/authorize',
'OAUTH2_API_BASE_URL' : 'https://id.example.com',
'OAUTH2_USERINFO_ENDPOINT' : 'https://id.example.com/api/oidc/userinfo',
'OAUTH2_SERVER_METADATA_URL' : 'https://id.example.com/.well-known/openid-configuration',
'OAUTH2_SCOPE' : 'openid email profile',
'OAUTH2_ICON' : 'fa-openid',
'OAUTH2_BUTTON_COLOR' : '#fd4b2d' # Can select any color you would like here.
}]
```

View File

@@ -0,0 +1,38 @@
---
id: portainer
---
# Portainer
**This requires Portainers Business Edition**
The following example variables are used, and should be replaced with your actual URLS.
- portainer.example.com (The url of your Portainer instance.)
- id.example.com (The url of your Pocket ID instance.)
## Pocket ID Setup
1. In Pocket-ID create a new OIDC Client, name it i.e. `Portainer`.
2. Set a logo for this OIDC Client if you would like too.
3. Set the callback URL to: `https://portainer.example.com/`.
4. Copy the `Client ID`, `Client Secret`, `Authorization URL`, `Userinfo URL`, and `Token URL` for use in the next steps.
# Portainer Setup
- While initally setting up OAuth in Portainer, its recommended to keep the `Hide internal authentication prompt` set to `Off` incase you need a fallback login
- This guide does **NOT** cover how to setup group claims in Portainer.
1. Open the Portainer web interface and navigate to: `Settings > Authentication`
2. Select `Custom OAuth Provider`
3. Paste the `Client ID` from Pocket ID into the `Client ID` field in Portainer.
4. Paste the `Client Secret` from Pocket ID into the `Client Secret` field in Portainer.
5. Paste the `Authorization URL` from Pocket ID into the `Authorization URL` field in Portainer.
6. Paste the `Token URL` from Pocket ID into the `Access token URL` field in Portainer.
7. Paste the `Userinfo URL` from Pocket ID into the `Resource URL` field in Portainer.
8. Set the `Redirect URL` to `https://portainer.example.com`
9. Set the `Logout URL` to `https://portainer.example.com`
10. Set the `User identifier` field to `preferred_username`. (This will use the users username vs the email)
11. Set the `Scopes` field to: `email openid profile`
12. Set `Auth Style` to `Auto detect`
13. Save the settings and test the new OAuth Login.

View File

@@ -0,0 +1,30 @@
---
id: proxmox
---
# Proxmox
The following example variables are used, and should be replaced with your actual URLS.
- proxmox.example.com (The url of your proxmox instance.)
- id.example.com (The url of your Pocket ID instance.)
## Pocket ID Setup
1. In Pocket-ID create a new OIDC Client, name it i.e. `Proxmox`.
2. Set a logo for this OIDC Client if you would like too.
3. Set the callback URL to: `https://proxmox.example.com`.
4. Copy the `Client ID`, and the `Client Secret` for use in the next steps.
## Proxmox Setup
1. Open the Proxmox Console and navigate to: `Datacenter - Realms`
2. Add a new `Open ID Connect Server` Realm
3. Enter `https://id.example.com` for the `Issuer URL`
4. Enter a name for the realm of your choice ie. `PocketID`
5. Paste the `Client ID` from Pocket ID into the `Client ID` field in Proxmox.
6. Paste the `Client Secret` from Pocket ID into the `Client Key` field in Proxmox.
7. You can check the `Default` box if you want this to be the deafult realm proxmox uses when signing in.
8. Check the `Autocreate Users` checkbox. (This will automaitcally create users in Proxmox if they dont exsist.)
9. Select `username` for the `Username Claim` dropdown (This is personal preference and controls how the username is shown, ie: `username = username@PocketID` or `email = username@example@PocketID`).
10. Leave the rest as defaults and click `OK` to save the new realm.

View File

@@ -0,0 +1,28 @@
---
id: semaphore-ui
---
# Semaphore UI
1. In Pocket-ID create a new OIDC Client, name it i.e. `Semaphore UI`.
2. Set the callback URL to: `https://<your-semaphore-ui-url>/api/auth/oidc/pocketid/redirect/`.
3. Add the following to your `config.json` file for Semaphore UI:
```json
"oidc_providers": {
"pocketid": {
"display_name": "Sign in with PocketID",
"provider_url": "https://<your-pocket-id-url>",
"client_id": "<client-id-from-pocket-id>",
"client_secret": "<client-secret-from-pocket-id>",
"redirect_url": "https://<your-semaphore-ui-url>/api/auth/oidc/pocketid/redirect/",
"scopes": [
"openid",
"profile",
"email"
],
"username_claim": "email",
"name_claim": "given_name"
}
}
```

View File

@@ -0,0 +1,22 @@
---
id: vikunja
---
# Vikunja
1. In Pocket-ID create a new OIDC Client, name it i.e. `Vikunja`
2. Set the callback url to: `https://<your-vikunja-subdomain>.<your-domain>/auth/openid/pocketid`
3. In `Vikunja` ensure to map a config file to your container, see [here](https://vikunja.io/docs/config-options/#using-a-config-file-with-docker-compose)
4. Add or set the following content to the `config.yml` file:
```yml
auth:
openid:
enabled: true
redirecturl: https://<your-vikunja-subdomain>.<your-domain>/auth/openid/pocketid
providers:
- name: Pocket-Id
authurl: https://<your-pocket-id-subdomain>.<your-domain>
clientid: <client id from the created OIDC client>
clientsecret: <client secret from the created OIDC client>
```

View File

@@ -0,0 +1,25 @@
---
id: environment-variables
---
# Environment Variables
Below are all the environment variables supported by Pocket ID. These should be configured in your `.env ` file.
Be cautious when modifying environment variables that are not recommended to change.
| Variable | Default Value | Recommended to change | Description |
| ---------------------------- | ------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PUBLIC_APP_URL` | `http://localhost` | yes | The URL where you will access the app. |
| `TRUST_PROXY` | `false` | yes | Whether the app is behind a reverse proxy. |
| `MAXMIND_LICENSE_KEY` | `-` | yes | License Key for the GeoLite2 Database. The license key is required to retrieve the geographical location of IP addresses in the audit log. If the key is not provided, IP locations will be marked as "unknown." You can obtain a license key for free [here](https://www.maxmind.com/en/geolite2/signup). |
| `PUID` and `PGID` | `1000` | yes | The user and group ID of the user who should run Pocket ID inside the Docker container and owns the files that are mounted with the volume. You can get the `PUID` and `GUID` of your user on your host machine by using the command `id`. For more information see [this article](https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables). |
| `DB_PROVIDER` | `sqlite` | no | The database provider you want to use. Currently `sqlite` and `postgres` are supported. |
| `SQLITE_DB_PATH` | `data/pocket-id.db` | no | The path to the SQLite database. This gets ignored if you didn't set `DB_PROVIDER` to `sqlite`. |
| `POSTGRES_CONNECTION_STRING` | `-` | no | The connection string to your Postgres database. This gets ignored if you didn't set `DB_PROVIDER` to `postgres`. A connection string can look like this: `postgresql://user:password@host:5432/pocket-id`. |
| `UPLOAD_PATH` | `data/uploads` | no | The path where the uploaded files are stored. |
| `INTERNAL_BACKEND_URL` | `http://localhost:8080` | no | The URL where the backend is accessible. |
| `GEOLITE_DB_PATH` | `data/GeoLite2-City.mmdb` | no | The path where the GeoLite2 database should be stored. |
| `CADDY_PORT` | `80` | no | The port on which Caddy should listen. Caddy is only active inside the Docker container. If you want to change the exposed port of the container then you sould change this variable. |
| `PORT` | `3000` | no | The port on which the frontend should listen. |
| `BACKEND_PORT` | `8080` | no | The port on which the backend should listen | |

View File

@@ -0,0 +1,40 @@
---
id: ldap
---
# LDAP Synchronization
Pocket ID can sync users and groups from an LDAP Source (lldap, OpenLDAP, Active Directory, etc.).
### LDAP Sync
- The LDAP Service will sync on Pocket ID startup and every hour once enabled from the Web UI.
- Users or groups synced from LDAP can **NOT** be edited from the Pocket ID Web UI.
### Generic LDAP Setup
1. Follow the installation guide [here](/pocket-id/setup/installation).
2. Once you have signed in with the initial admin account, navigate to the Application Configuration section at `https://pocket.id/settings/admin/application-configuration`.
3. Client Configuration Setup
| LDAP Variable | Example Value | Description |
| ------------------ | ---------------------------------- | ------------------------------------------------------------- |
| LDAP URL | ldaps://ldap.mydomain.com:636 | The URL with port to connect to LDAP |
| LDAP Bind DN | cn=admin,ou=users,dc=domain,dc=com | The full DN value for the user with search privileges in LDAP |
| LDAP Bind Password | securepassword | The password for the Bind DN account |
| LDAP Search Base | dc=domain,dc=com | The top-level path to search for users and groups |
<br />
4. LDAP Attribute Configuration Setup
| LDAP Variable | Example Value | Description |
| --------------------------------- | ------------------ | -------------------------------------------------------------------------------- |
| User Unique Identifier Attribute | uuid | The LDAP attribute to uniquely identify the user, **this should never change** |
| Username Attribute | uid | The LDAP attribute to use as the username of users |
| User Mail Attribute | mail | The LDAP attribute to use for the email of users |
| User First Name Attribute | givenName | The LDAP attribute to use for the first name of users |
| User Last Name Attribute | sn | The LDAP attribute to use for the last name of users |
| Group Unique Identifier Attribute | uuid | The LDAP attribute to uniquely identify the groups, **this should never change** |
| Group Name Attribute | uid | The LDAP attribute to use as the name of synced groups |
| Admin Group Name | \_pocket_id_admins | The group name to use for admin permissions for LDAP users |

View File

@@ -1,4 +1,8 @@
# Proxy Services through Pocket ID
---
id: proxy-services
---
# Proxy Services
The goal of Pocket ID is to function exclusively as an OIDC provider. As such, we don't have a built-in proxy provider. However, you can use other tools that act as a middleware to protect your services and support OIDC as an authentication provider.
@@ -33,13 +37,14 @@ caddy add-package github.com/greenpau/caddy-security
```bash
{
# Port to listen on
# Port to listen on
http_port 443
# Configure caddy-security.
# Configure caddy-security.
order authenticate before respond
security {
oauth identity provider generic {
delay_start 3
realm generic
driver generic
client_id client-id-from-pocket-id # Replace with your own client ID

24
docs/docs/introduction.md Normal file
View File

@@ -0,0 +1,24 @@
---
id: introduction
---
# Introduction
Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.
The goal of Pocket ID is to be a simple and easy-to-use. There are other self-hosted OIDC providers like [Keycloak](https://www.keycloak.org/) or [ORY Hydra](https://www.ory.sh/hydra/) but they are often too complex for simple use cases.
Additionally, what makes Pocket ID special is that it only supports [passkey](https://www.passkeys.io/) authentication, which means you dont need a password. Some people might not like this idea at first, but I believe passkeys are the future, and once you try them, youll love them. For example, you can now use a physical Yubikey to sign in to all your self-hosted services easily and securely.
**_Pocket ID is in its early stages and may contain bugs. There might be OIDC features that are not yet implemented. If you encounter any issues, please open an issue_** [here](https://github.com/stonith404/pocket-id/issues/new?template=bug.yml).
## Get to know Pocket ID
→ [Try the Demo of Pocket ID](https://pocket-id.eliasschneider.com/)<br/>
<img src="https://github.com/user-attachments/assets/96ac549d-b897-404a-8811-f42b16ea58e2" width="700"/>
## Useful Links
- [Installation](/pocket-id/setup/installation)
- [Proxy Services](/pocket-id/guides/proxy-services)
- [Client Examples](/pocket-id/client-examples)

View File

@@ -0,0 +1,73 @@
---
id: installation
---
# Installation
# Before you start
Pocket ID requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts), meaning it must be served over HTTPS. This is necessary because Pocket ID uses the [WebAuthn API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API).
### Installation with Docker (recommended)
1. Download the `docker-compose.yml` and `.env` file:
```bash
curl -O https://raw.githubusercontent.com/stonith404/pocket-id/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/stonith404/pocket-id/main/.env.example
```
2. Edit the `.env` file so that it fits your needs. See the [environment variables](/pocket-id/configuration/environment-variables) section for more information.
3. Run `docker compose up -d`
You can now sign in with the admin account on `http://localhost/login/setup`.
### Unraid
Pocket ID is available as a template on the Community Apps store.
### Stand-alone Installation
Required tools:
- [Node.js](https://nodejs.org/en/download/) >= 20
- [Go](https://golang.org/doc/install) >= 1.23
- [Git](https://git-scm.com/downloads)
- [PM2](https://pm2.keymetrics.io/)
- [Caddy](https://caddyserver.com/docs/install) (optional)
1. Copy the `.env.example` file in the `frontend` and `backend` folder to `.env` and change it so that it fits your needs.
```bash
cp frontend/.env.example frontend/.env
cp backend/.env.example backend/.env
```
2. Run the following commands:
```bash
git clone https://github.com/stonith404/pocket-id
cd pocket-id
# Checkout the latest version
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
# Start the backend
cd backend/cmd
go build -o ../pocket-id-backend
cd ..
pm2 start pocket-id-backend --name pocket-id-backend
# Start the frontend
cd ../frontend
npm install
npm run build
pm2 start --name pocket-id-frontend --node-args="--env-file .env" build/index.js
# Optional: Start Caddy (You can use any other reverse proxy)
cd ..
pm2 start --name pocket-id-caddy caddy -- run --config reverse-proxy/Caddyfile
```
You can now sign in with the admin account on `http://localhost/login/setup`.

View File

@@ -0,0 +1,13 @@
---
id: nginx-reverse-proxy
---
# Nginx Reverse Proxy
To use Nginx as a reverse proxy for Pocket ID, update the configuration to increase the header buffer size. This adjustment is necessary because SvelteKit generates larger headers, which may exceed the default buffer limits.
```nginx
proxy_busy_buffers_size 512k;
proxy_buffers 4 512k;
proxy_buffer_size 256k;
```

View File

@@ -0,0 +1,45 @@
---
id: upgrading
---
# Upgrading
Updating to a New Version
#### Docker
```bash
docker compose pull
docker compose up -d
```
#### Stand-alone
1. Stop the running services:
```bash
pm2 delete pocket-id-backend pocket-id-frontend pocket-id-caddy
```
2. Run the following commands:
```bash
cd pocket-id
# Checkout the latest version
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
# Start the backend
cd backend/cmd
go build -o ../pocket-id-backend
cd ..
pm2 start pocket-id-backend --name pocket-id-backend
# Start the frontend
cd ../frontend
npm install
npm run build
pm2 start build/index.js --name pocket-id-frontend
# Optional: Start Caddy (You can use any other reverse proxy)
cd ..
pm2 start caddy --name pocket-id-caddy -- run --config reverse-proxy/Caddyfile
```

View File

@@ -0,0 +1,13 @@
---
id: account-recovery
---
# Account recovery
There are two ways to create a one-time access link for a user:
1. **UI**: An admin can create a one-time access link for the user in the admin panel under the "Users" tab by clicking on the three dots next to the user's name and selecting "One-time link".
2. **Terminal**: You can create a one-time access link for a user by running the `scripts/create-one-time-access-token.sh` script. To execute this script with Docker you have to run the following command:
```bash
docker compose exec pocket-id sh "sh scripts/create-one-time-access-token.sh <username or email>"
```

64
docs/docusaurus.config.ts Normal file
View File

@@ -0,0 +1,64 @@
import type * as Preset from "@docusaurus/preset-classic";
import type { Config } from "@docusaurus/types";
import { themes as prismThemes } from "prism-react-renderer";
const config: Config = {
title: "Pocket ID",
tagline:
"Pocket ID is a simple OIDC provider that allows users to authenticate with their passkeys to your services.",
favicon: "img/pocket-id.png",
url: "https://stonith404.github.io",
baseUrl: "/pocket-id/",
organizationName: "stonith404",
projectName: "pocket-id",
onBrokenLinks: "warn",
onBrokenMarkdownLinks: "warn",
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"classic",
{
docs: {
routeBasePath: "/",
sidebarPath: "./sidebars.ts",
editUrl: "https://github.com/stonith404/pocket-id/edit/main/docs",
},
blog: false,
} satisfies Preset.Options,
],
],
themeConfig: {
image: "img/pocket-id.png",
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: "Pocket ID",
logo: {
alt: "Pocket ID Share Logo",
src: "img/pocket-id.png",
},
items: [
{
href: "https://github.com/stonith404/pocket-id",
label: "GitHub",
position: "right",
},
],
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
} satisfies Preset.ThemeConfig,
};
export default config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

17969
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
docs/package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "pocket-id-docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "GIT_USER=stonith404 docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.7.0",
"@docusaurus/preset-classic": "3.7.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.7.0",
"@docusaurus/tsconfig": "3.7.0",
"@docusaurus/types": "3.7.0",
"typescript": "~5.6.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}

104
docs/sidebars.ts Normal file
View File

@@ -0,0 +1,104 @@
import type { SidebarsConfig } from "@docusaurus/plugin-content-docs";
const sidebars: SidebarsConfig = {
docsSidebar: [
{
type: "doc",
id: "introduction",
},
{
type: "category",
label: "Getting Started",
items: [
{
type: "doc",
id: "setup/installation",
},
{
type: "doc",
id: "setup/nginx-reverse-proxy",
},
{
type: "doc",
id: "setup/upgrading",
},
],
},
{
type: "category",
label: "Configuration",
items: [
{
type: "doc",
id: "configuration/environment-variables",
},
{
type: "doc",
id: "configuration/ldap",
},
],
},
{
type: "category",
label: "Guides",
items: [
{
type: "doc",
id: "guides/proxy-services",
},
],
},
{
type: "category",
label: "Client Examples",
link: {
type: "generated-index",
title: "Client Examples",
description:
"Examples of how to setup Pocket ID with different clients",
slug: "client-examples",
},
items: [
"client-examples/cloudflare-zero-trust",
"client-examples/grist",
"client-examples/hoarder",
"client-examples/jellyfin",
"client-examples/netbox",
"client-examples/open-webui",
"client-examples/pgadmin",
"client-examples/portainer",
"client-examples/proxmox",
"client-examples/semaphore-ui",
"client-examples/vikunja",
],
},
{
type: "category",
label: "Troubleshooting",
items: [
{
type: "doc",
id: "troubleshooting/account-recovery",
},
],
},
{
type: "category",
label: "Helping Out",
items: [
{
type: "link",
label: "Contributing",
href: "https://github.com/stonith404/pocket-id/blob/main/CONTRIBUTING.md",
},
],
},
{
type: "link",
label: "Demo",
href: "https://pocket-id.eliasschneider.com/",
},
],
};
export default sidebars;

6
docs/src/pages/index.tsx Normal file
View File

@@ -0,0 +1,6 @@
import React from "react";
import { Redirect } from "react-router-dom";
export default function Home() {
return <Redirect to="/pocket-id/introduction" />;
}

0
docs/static/.nojekyll vendored Normal file
View File

BIN
docs/static/img/pocket-id.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

8
docs/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": "."
},
"exclude": [".docusaurus", "build"]
}

View File

@@ -1,2 +1,3 @@
PUBLIC_APP_URL=http://localhost
# /!\ If PUBLIC_APP_URL is not a localhost address, it must be HTTPS
INTERNAL_BACKEND_URL=http://localhost:8080

View File

@@ -1,6 +1,6 @@
{
"name": "pocket-id-frontend",
"version": "0.25.1",
"version": "0.27.1",
"private": true,
"scripts": {
"dev": "vite dev --port 3000",

View File

@@ -1,4 +1,5 @@
import { env } from '$env/dynamic/private';
import { ACCESS_TOKEN_COOKIE_NAME } from '$lib/constants';
import type { Handle, HandleServerError } from '@sveltejs/kit';
import { AxiosError } from 'axios';
import jwt from 'jsonwebtoken';
@@ -9,7 +10,7 @@ import jwt from 'jsonwebtoken';
process.env.INTERNAL_BACKEND_URL = env.INTERNAL_BACKEND_URL ?? 'http://localhost:8080';
export const handle: Handle = async ({ event, resolve }) => {
const accessToken = event.cookies.get('access_token');
const accessToken = event.cookies.get(ACCESS_TOKEN_COOKIE_NAME);
let isSignedIn: boolean = false;
let isAdmin: boolean = false;

View File

@@ -0,0 +1,2 @@
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,10 +1,11 @@
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 userService = new UserService(cookies.get('access_token'));
const appConfigService = new AppConfigService(cookies.get('access_token'));
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const appConfigService = new AppConfigService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const user = await userService
.getCurrent()

View File

@@ -1,9 +1,10 @@
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 ({ url, cookies }) => {
const clientId = url.searchParams.get('client_id');
const oidcService = new OidcService(cookies.get('access_token'));
const oidcService = new OidcService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const client = await oidcService.getClient(clientId!);

View File

@@ -1,10 +1,11 @@
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 webauthnService = new WebAuthnService(cookies.get('access_token'));
const userService = new UserService(cookies.get('access_token'));
const webauthnService = new WebAuthnService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const account = await userService.getCurrent();
const passkeys = await webauthnService.listCredentials();
return {

View File

@@ -1,8 +1,9 @@
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'));
const appConfigService = new AppConfigService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const appConfig = await appConfigService.list(true);
return { appConfig };
};

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import CheckboxWithLabel from '$lib/components/checkbox-with-label.svelte';
import { openConfirmDialog } from '$lib/components/confirm-dialog';
import FormInput from '$lib/components/form-input.svelte';
import { Button } from '$lib/components/ui/button';
import AppConfigService from '$lib/services/app-config-service';
@@ -20,18 +21,6 @@
let isSendingTestEmail = $state(false);
const updatedAppConfig = {
smtpHost: appConfig.smtpHost,
smtpPort: appConfig.smtpPort,
smtpUser: appConfig.smtpUser,
smtpPassword: appConfig.smtpPassword,
smtpFrom: appConfig.smtpFrom,
smtpTls: appConfig.smtpTls,
smtpSkipCertVerify: appConfig.smtpSkipCertVerify,
emailOneTimeAccessEnabled: appConfig.emailOneTimeAccessEnabled,
emailLoginNotificationEnabled: appConfig.emailLoginNotificationEnabled
};
const formSchema = z.object({
smtpHost: z.string().min(1),
smtpPort: z.number().min(1),
@@ -44,17 +33,47 @@
emailLoginNotificationEnabled: z.boolean()
});
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, updatedAppConfig);
const { inputs, ...form } = createForm<typeof formSchema>(formSchema, appConfig);
async function onSubmit() {
const data = form.validate();
if (!data) return false;
await callback(data);
// Update the app config to don't display the unsaved changes warning
Object.entries(data).forEach(([key, value]) => {
// @ts-ignore
appConfig[key] = value;
});
toast.success('Email configuration updated successfully');
return true;
}
async function onTestEmail() {
// @ts-ignore
const hasChanges = Object.keys($inputs).some((key) => $inputs[key].value !== appConfig[key]);
if (hasChanges) {
openConfirmDialog({
title: 'Save changes?',
message:
'You have to save the changes before sending a test email. Do you want to save now?',
confirm: {
label: 'Save and send',
action: async () => {
const saved = await onSubmit();
if (saved) {
sendTestEmail();
}
}
}
});
} else {
sendTestEmail();
}
}
async function sendTestEmail() {
isSendingTestEmail = true;
await appConfigService
.sendTestEmail()
@@ -105,7 +124,7 @@
<div class="mt-8 flex flex-wrap justify-end gap-3">
<Button isLoading={isSendingTestEmail} variant="secondary" onclick={onTestEmail}
>Send Test Email</Button
>Send test email</Button
>
<Button type="submit">Save</Button>
</div>

View File

@@ -1,8 +1,9 @@
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 ({ cookies }) => {
const oidcService = new OIDCService(cookies.get('access_token'));
const oidcService = new OIDCService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const clients = await oidcService.listClients();
return clients;
};

View File

@@ -1,7 +1,8 @@
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'));
const oidcService = new OidcService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
return await oidcService.getClient(params.id);
};

View File

@@ -36,7 +36,7 @@
const formSchema = z.object({
name: z.string().min(2).max(50),
callbackURLs: z.array(z.string().url()).nonempty(),
callbackURLs: z.array(z.string()).nonempty(),
isPublic: z.boolean(),
pkceEnabled: z.boolean()
});

View File

@@ -1,8 +1,9 @@
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 ({ cookies }) => {
const userGroupService = new UserGroupService(cookies.get('access_token'));
const userGroupService = new UserGroupService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const userGroups = await userGroupService.list();
return userGroups;
};

View File

@@ -1,8 +1,9 @@
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'));
const userGroupService = new UserGroupService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const userGroup = await userGroupService.get(params.id);
return { userGroup };

View File

@@ -1,8 +1,9 @@
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 ({ cookies }) => {
const userService = new UserService(cookies.get('access_token'));
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const users = await userService.list();
return users;
};

View File

@@ -1,8 +1,9 @@
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'));
const userService = new UserService(cookies.get(ACCESS_TOKEN_COOKIE_NAME));
const user = await userService.get(params.id);
return user;
};

View File

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

View File

@@ -2,9 +2,4 @@
reverse_proxy /api/* http://localhost:{$BACKEND_PORT:8080}
reverse_proxy /.well-known/* http://localhost:{$BACKEND_PORT:8080}
reverse_proxy /* http://localhost:{$PORT:3000}
log {
output file /var/log/caddy/access.log
level WARN
}
}

View File

@@ -8,9 +8,4 @@
reverse_proxy /* http://localhost:{$PORT:3000} {
trusted_proxies 0.0.0.0/0
}
log {
output file /var/log/caddy/access.log
level WARN
}
}

View File

@@ -1,3 +1,9 @@
# Check if the script is being run from the root of the project
if [ ! -f .version ] || [ ! -f frontend/package.json ] || [ ! -f CHANGELOG.md ]; then
echo "Error: This script must be run from the root of the project."
exit 1
fi
# Read the current version from .version
VERSION=$(cat .version)
@@ -29,6 +35,13 @@ else
exit 1
fi
# Confirm release creation
read -p "This will create a new $RELEASE_TYPE release with version $NEW_VERSION. Do you want to proceed? (y/n) " CONFIRM
if [[ "$CONFIRM" != "y" ]]; then
echo "Release process canceled."
exit 1
fi
# Update the .version file with the new version
echo $NEW_VERSION >.version
git add .version
@@ -58,4 +71,30 @@ git tag "v$NEW_VERSION"
git push
git push --tags
# Check if GitHub CLI is installed
if ! command -v gh &>/dev/null; then
echo "GitHub CLI (gh) is not installed. Please install it and authenticate using 'gh auth login'."
exit 1
fi
# Extract the changelog content for the latest release
echo "Extracting changelog content for version $NEW_VERSION..."
CHANGELOG=$(awk '/^## / {if (NR > 1) exit} NR > 1 {print}' CHANGELOG.md | awk 'NR > 2 || NF {print}')
if [ -z "$CHANGELOG" ]; then
echo "Error: Could not extract changelog for version $NEW_VERSION."
exit 1
fi
# Create the release on GitHub
echo "Creating GitHub release..."
gh release create "v$NEW_VERSION" --title "v$NEW_VERSION" --notes "$CHANGELOG"
if [ $? -eq 0 ]; then
echo "GitHub release created successfully."
else
echo "Error: Failed to create GitHub release."
exit 1
fi
echo "Release process complete. New version: $NEW_VERSION"