mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-08 17:23:23 +03:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f55e6e54 | ||
|
|
a4bfd08a0f | ||
|
|
7b654c6bd1 | ||
|
|
8c1c04db1d | ||
|
|
ec4b41a1d2 | ||
|
|
d27a121985 | ||
|
|
d8952c0d62 | ||
|
|
f65997e85b |
32
.devcontainer/devcontainer.json
Normal file
32
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
|
||||||
|
{
|
||||||
|
"name": "pocket-id",
|
||||||
|
// 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",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/go:1": {},
|
||||||
|
"ghcr.io/devcontainers-extra/features/caddy:1": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"golang.go",
|
||||||
|
"svelte.svelte-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// Install npm dependencies for the frontend.
|
||||||
|
"postCreateCommand": "npm install --prefix frontend"
|
||||||
|
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,3 +48,6 @@ pocket-id-backend
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
#Debug
|
||||||
|
backend/cmd/__debug_*
|
||||||
|
|||||||
42
.vscode/launch.json
vendored
Normal file
42
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Backend",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"envFile": "${workspaceFolder}/backend/.env.example",
|
||||||
|
"env": {
|
||||||
|
"APP_ENV": "development"
|
||||||
|
},
|
||||||
|
"mode": "debug",
|
||||||
|
"program": "${workspaceFolder}/backend/cmd/main.go",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Frontend",
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"envFile": "${workspaceFolder}/frontend/.env.example",
|
||||||
|
"cwd": "${workspaceFolder}/frontend",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compounds": [
|
||||||
|
{
|
||||||
|
"name": "Development",
|
||||||
|
"configurations": [
|
||||||
|
"Backend",
|
||||||
|
"Frontend"
|
||||||
|
],
|
||||||
|
"presentation": {
|
||||||
|
"hidden": false,
|
||||||
|
"group": "",
|
||||||
|
"order": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
37
.vscode/tasks.json
vendored
Normal file
37
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,3 +1,15 @@
|
|||||||
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.39.0...v) (2025-03-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* allow setting path where keys are stored ([#327](https://github.com/pocket-id/pocket-id/issues/327)) ([7b654c6](https://github.com/pocket-id/pocket-id/commit/7b654c6bd111ddcddd5e3450cbf326d9cf1777b6))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **docker:** missing write permissions on scripts ([ec4b41a](https://github.com/pocket-id/pocket-id/commit/ec4b41a1d26ea00bb4a95f654ac4cc745b2ce2e8))
|
||||||
|
|
||||||
## [](https://github.com/pocket-id/pocket-id/compare/v0.38.0...v) (2025-03-11)
|
## [](https://github.com/pocket-id/pocket-id/compare/v0.38.0...v) (2025-03-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,15 @@ 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.
|
## 1. Using DevContainers
|
||||||
|
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
|
||||||
|
3. VS Code will detect .devcontainer and will prompt you to open the folder in devcontainer
|
||||||
|
4. If the auto prompt does not work, hit `F1` and select `Dev Containers: Open Folder in Container.`, then select the pocket-id repo root folder and it'll open in container.
|
||||||
|
|
||||||
|
## 2. Manual
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
@@ -63,6 +70,10 @@ Run `caddy run --config reverse-proxy/Caddyfile` in the root folder.
|
|||||||
|
|
||||||
You're all set!
|
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.
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
|
|||||||
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
||||||
|
|
||||||
COPY ./scripts ./scripts
|
COPY ./scripts ./scripts
|
||||||
RUN chmod +x ./scripts/*.sh
|
RUN chmod +x ./scripts/**/*.sh
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
ENV APP_ENV=production
|
ENV APP_ENV=production
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mileusna/useragent v1.3.5
|
github.com/mileusna/useragent v1.3.5
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.2
|
||||||
golang.org/x/crypto v0.32.0
|
golang.org/x/crypto v0.35.0
|
||||||
golang.org/x/image v0.24.0
|
golang.org/x/image v0.24.0
|
||||||
golang.org/x/time v0.9.0
|
golang.org/x/time v0.9.0
|
||||||
gorm.io/driver/postgres v1.5.11
|
gorm.io/driver/postgres v1.5.11
|
||||||
@@ -69,9 +69,9 @@ require (
|
|||||||
go.uber.org/atomic v1.11.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.13.0 // indirect
|
golang.org/x/arch v0.13.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.36.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.29.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.4 // indirect
|
google.golang.org/protobuf v1.36.4 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
@@ -240,8 +240,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -263,8 +263,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type EnvConfigSchema struct {
|
|||||||
SqliteDBPath string `env:"SQLITE_DB_PATH"`
|
SqliteDBPath string `env:"SQLITE_DB_PATH"`
|
||||||
PostgresConnectionString string `env:"POSTGRES_CONNECTION_STRING"`
|
PostgresConnectionString string `env:"POSTGRES_CONNECTION_STRING"`
|
||||||
UploadPath string `env:"UPLOAD_PATH"`
|
UploadPath string `env:"UPLOAD_PATH"`
|
||||||
|
KeysPath string `env:"KEYS_PATH"`
|
||||||
Port string `env:"BACKEND_PORT"`
|
Port string `env:"BACKEND_PORT"`
|
||||||
Host string `env:"HOST"`
|
Host string `env:"HOST"`
|
||||||
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
MaxMindLicenseKey string `env:"MAXMIND_LICENSE_KEY"`
|
||||||
@@ -37,6 +38,7 @@ var EnvConfig = &EnvConfigSchema{
|
|||||||
SqliteDBPath: "data/pocket-id.db",
|
SqliteDBPath: "data/pocket-id.db",
|
||||||
PostgresConnectionString: "",
|
PostgresConnectionString: "",
|
||||||
UploadPath: "data/uploads",
|
UploadPath: "data/uploads",
|
||||||
|
KeysPath: "data/keys",
|
||||||
AppURL: "http://localhost",
|
AppURL: "http://localhost",
|
||||||
Port: "8080",
|
Port: "8080",
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
@@ -50,19 +52,21 @@ func init() {
|
|||||||
if err := env.ParseWithOptions(EnvConfig, env.Options{}); err != nil {
|
if err := env.ParseWithOptions(EnvConfig, env.Options{}); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the environment variables
|
// Validate the environment variables
|
||||||
if EnvConfig.DbProvider != DbProviderSqlite && EnvConfig.DbProvider != DbProviderPostgres {
|
switch EnvConfig.DbProvider {
|
||||||
|
case DbProviderSqlite:
|
||||||
|
if EnvConfig.SqliteDBPath == "" {
|
||||||
|
log.Fatal("Missing SQLITE_DB_PATH environment variable")
|
||||||
|
}
|
||||||
|
case DbProviderPostgres:
|
||||||
|
if EnvConfig.PostgresConnectionString == "" {
|
||||||
|
log.Fatal("Missing POSTGRES_CONNECTION_STRING environment variable")
|
||||||
|
}
|
||||||
|
default:
|
||||||
log.Fatal("Invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'")
|
log.Fatal("Invalid DB_PROVIDER value. Must be 'sqlite' or 'postgres'")
|
||||||
}
|
}
|
||||||
|
|
||||||
if EnvConfig.DbProvider == DbProviderPostgres && EnvConfig.PostgresConnectionString == "" {
|
|
||||||
log.Fatal("Missing POSTGRES_CONNECTION_STRING environment variable")
|
|
||||||
}
|
|
||||||
|
|
||||||
if EnvConfig.DbProvider == DbProviderSqlite && EnvConfig.SqliteDBPath == "" {
|
|
||||||
log.Fatal("Missing SQLITE_DB_PATH environment variable")
|
|
||||||
}
|
|
||||||
|
|
||||||
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("PUBLIC_APP_URL is not a valid URL")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
@@ -22,13 +23,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
privateKeyPath = "data/keys/jwt_private_key.pem"
|
privateKeyFile = "jwt_private_key.pem"
|
||||||
publicKeyPath = "data/keys/jwt_public_key.pem"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type JwtService struct {
|
type JwtService struct {
|
||||||
PublicKey *rsa.PublicKey
|
privateKey *rsa.PrivateKey
|
||||||
PrivateKey *rsa.PrivateKey
|
keyId string
|
||||||
appConfigService *AppConfigService
|
appConfigService *AppConfigService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ func NewJwtService(appConfigService *AppConfigService) *JwtService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure keys are generated or loaded
|
// Ensure keys are generated or loaded
|
||||||
if err := service.loadOrGenerateKeys(); err != nil {
|
if err := service.loadOrGenerateKey(common.EnvConfig.KeysPath); err != nil {
|
||||||
log.Fatalf("Failed to initialize jwt service: %v", err)
|
log.Fatalf("Failed to initialize jwt service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,30 +59,39 @@ type JWK struct {
|
|||||||
E string `json:"e"`
|
E string `json:"e"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadOrGenerateKeys loads RSA keys from the given paths or generates them if they do not exist.
|
// loadOrGenerateKey loads RSA keys from the given paths or generates them if they do not exist.
|
||||||
func (s *JwtService) loadOrGenerateKeys() error {
|
func (s *JwtService) loadOrGenerateKey(keysPath string) error {
|
||||||
|
privateKeyPath := filepath.Join(keysPath, privateKeyFile)
|
||||||
|
|
||||||
if _, err := os.Stat(privateKeyPath); os.IsNotExist(err) {
|
if _, err := os.Stat(privateKeyPath); os.IsNotExist(err) {
|
||||||
if err := s.generateKeys(); err != nil {
|
if err := s.generateKey(keysPath); err != nil {
|
||||||
return err
|
return fmt.Errorf("can't generate key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKeyBytes, err := os.ReadFile(privateKeyPath)
|
privateKeyBytes, err := os.ReadFile(privateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("can't read jwt private key: " + err.Error())
|
return fmt.Errorf("can't read jwt private key: %w", err)
|
||||||
}
|
}
|
||||||
s.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes)
|
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("can't parse jwt private key: " + err.Error())
|
return fmt.Errorf("can't parse jwt private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
publicKeyBytes, err := os.ReadFile(publicKeyPath)
|
err = s.SetKey(privateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("can't read jwt public key: " + err.Error())
|
return fmt.Errorf("failed to set private key: %w", err)
|
||||||
}
|
}
|
||||||
s.PublicKey, err = jwt.ParseRSAPublicKeyFromPEM(publicKeyBytes)
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *JwtService) SetKey(privateKey *rsa.PrivateKey) (err error) {
|
||||||
|
s.privateKey = privateKey
|
||||||
|
|
||||||
|
s.keyId, err = s.generateKeyID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("can't parse jwt public key: " + err.Error())
|
return fmt.Errorf("can't generate key ID: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -100,20 +109,15 @@ func (s *JwtService) GenerateAccessToken(user model.User) (string, error) {
|
|||||||
IsAdmin: user.IsAdmin,
|
IsAdmin: user.IsAdmin,
|
||||||
}
|
}
|
||||||
|
|
||||||
kid, err := s.generateKeyID(s.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("failed to generate key ID: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim)
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim)
|
||||||
token.Header["kid"] = kid
|
token.Header["kid"] = s.keyId
|
||||||
|
|
||||||
return token.SignedString(s.PrivateKey)
|
return token.SignedString(s.privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JwtService) VerifyAccessToken(tokenString string) (*AccessTokenJWTClaims, error) {
|
func (s *JwtService) VerifyAccessToken(tokenString string) (*AccessTokenJWTClaims, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &AccessTokenJWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &AccessTokenJWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return s.PublicKey, nil
|
return &s.privateKey.PublicKey, nil
|
||||||
})
|
})
|
||||||
if err != nil || !token.Valid {
|
if err != nil || !token.Valid {
|
||||||
return nil, errors.New("couldn't handle this token")
|
return nil, errors.New("couldn't handle this token")
|
||||||
@@ -146,15 +150,10 @@ func (s *JwtService) GenerateIDToken(userClaims map[string]interface{}, clientID
|
|||||||
claims["nonce"] = nonce
|
claims["nonce"] = nonce
|
||||||
}
|
}
|
||||||
|
|
||||||
kid, err := s.generateKeyID(s.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("failed to generate key ID: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
token.Header["kid"] = kid
|
token.Header["kid"] = s.keyId
|
||||||
|
|
||||||
return token.SignedString(s.PrivateKey)
|
return token.SignedString(s.privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JwtService) GenerateOauthAccessToken(user model.User, clientID string) (string, error) {
|
func (s *JwtService) GenerateOauthAccessToken(user model.User, clientID string) (string, error) {
|
||||||
@@ -166,20 +165,15 @@ func (s *JwtService) GenerateOauthAccessToken(user model.User, clientID string)
|
|||||||
Issuer: common.EnvConfig.AppURL,
|
Issuer: common.EnvConfig.AppURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
kid, err := s.generateKeyID(s.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.New("failed to generate key ID: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim)
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claim)
|
||||||
token.Header["kid"] = kid
|
token.Header["kid"] = s.keyId
|
||||||
|
|
||||||
return token.SignedString(s.PrivateKey)
|
return token.SignedString(s.privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *JwtService) VerifyOauthAccessToken(tokenString string) (*jwt.RegisteredClaims, error) {
|
func (s *JwtService) VerifyOauthAccessToken(tokenString string) (*jwt.RegisteredClaims, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return s.PublicKey, nil
|
return &s.privateKey.PublicKey, nil
|
||||||
})
|
})
|
||||||
if err != nil || !token.Valid {
|
if err != nil || !token.Valid {
|
||||||
return nil, errors.New("couldn't handle this token")
|
return nil, errors.New("couldn't handle this token")
|
||||||
@@ -195,7 +189,7 @@ func (s *JwtService) VerifyOauthAccessToken(tokenString string) (*jwt.Registered
|
|||||||
|
|
||||||
func (s *JwtService) VerifyIdToken(tokenString string) (*jwt.RegisteredClaims, error) {
|
func (s *JwtService) VerifyIdToken(tokenString string) (*jwt.RegisteredClaims, error) {
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||||
return s.PublicKey, nil
|
return &s.privateKey.PublicKey, nil
|
||||||
}, jwt.WithIssuer(common.EnvConfig.AppURL))
|
}, jwt.WithIssuer(common.EnvConfig.AppURL))
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, jwt.ErrTokenExpired) {
|
if err != nil && !errors.Is(err, jwt.ErrTokenExpired) {
|
||||||
@@ -212,32 +206,27 @@ func (s *JwtService) VerifyIdToken(tokenString string) (*jwt.RegisteredClaims, e
|
|||||||
|
|
||||||
// GetJWK returns the JSON Web Key (JWK) for the public key.
|
// GetJWK returns the JSON Web Key (JWK) for the public key.
|
||||||
func (s *JwtService) GetJWK() (JWK, error) {
|
func (s *JwtService) GetJWK() (JWK, error) {
|
||||||
if s.PublicKey == nil {
|
if s.privateKey == nil {
|
||||||
return JWK{}, errors.New("public key is not initialized")
|
return JWK{}, errors.New("public key is not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
kid, err := s.generateKeyID(s.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return JWK{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
jwk := JWK{
|
jwk := JWK{
|
||||||
Kid: kid,
|
Kid: s.keyId,
|
||||||
Kty: "RSA",
|
Kty: "RSA",
|
||||||
Use: "sig",
|
Use: "sig",
|
||||||
Alg: "RS256",
|
Alg: "RS256",
|
||||||
N: base64.RawURLEncoding.EncodeToString(s.PublicKey.N.Bytes()),
|
N: base64.RawURLEncoding.EncodeToString(s.privateKey.N.Bytes()),
|
||||||
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(s.PublicKey.E)).Bytes()),
|
E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(s.privateKey.E)).Bytes()),
|
||||||
}
|
}
|
||||||
|
|
||||||
return jwk, nil
|
return jwk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateKeyID generates a Key ID for the public key using the first 8 bytes of the SHA-256 hash of the public key.
|
// GenerateKeyID generates a Key ID for the public key using the first 8 bytes of the SHA-256 hash of the public key.
|
||||||
func (s *JwtService) generateKeyID(publicKey *rsa.PublicKey) (string, error) {
|
func (s *JwtService) generateKeyID() (string, error) {
|
||||||
pubASN1, err := x509.MarshalPKIXPublicKey(publicKey)
|
pubASN1, err := x509.MarshalPKIXPublicKey(&s.privateKey.PublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.New("failed to marshal public key: " + err.Error())
|
return "", fmt.Errorf("failed to marshal public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute SHA-256 hash of the public key
|
// Compute SHA-256 hash of the public key
|
||||||
@@ -252,29 +241,22 @@ func (s *JwtService) generateKeyID(publicKey *rsa.PublicKey) (string, error) {
|
|||||||
return base64.RawURLEncoding.EncodeToString(shortHash), nil
|
return base64.RawURLEncoding.EncodeToString(shortHash), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateKeys generates a new RSA key pair and saves them to the specified paths.
|
// generateKey generates a new RSA key and saves it to the specified path.
|
||||||
func (s *JwtService) generateKeys() error {
|
func (s *JwtService) generateKey(keysPath string) error {
|
||||||
if err := os.MkdirAll(filepath.Dir(privateKeyPath), 0700); err != nil {
|
if err := os.MkdirAll(keysPath, 0700); err != nil {
|
||||||
return errors.New("failed to create directories for keys: " + err.Error())
|
return fmt.Errorf("failed to create directories for keys: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to generate private key: " + err.Error())
|
return fmt.Errorf("failed to generate private key: %w", err)
|
||||||
}
|
}
|
||||||
s.PrivateKey = privateKey
|
|
||||||
|
|
||||||
|
privateKeyPath := filepath.Join(keysPath, privateKeyFile)
|
||||||
if err := s.savePEMKey(privateKeyPath, x509.MarshalPKCS1PrivateKey(privateKey), "RSA PRIVATE KEY"); err != nil {
|
if err := s.savePEMKey(privateKeyPath, x509.MarshalPKCS1PrivateKey(privateKey), "RSA PRIVATE KEY"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
publicKey := &privateKey.PublicKey
|
|
||||||
s.PublicKey = publicKey
|
|
||||||
|
|
||||||
if err := s.savePEMKey(publicKeyPath, x509.MarshalPKCS1PublicKey(publicKey), "RSA PUBLIC KEY"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +264,7 @@ func (s *JwtService) generateKeys() error {
|
|||||||
func (s *JwtService) savePEMKey(path string, keyBytes []byte, keyType string) error {
|
func (s *JwtService) savePEMKey(path string, keyBytes []byte, keyType string) error {
|
||||||
keyFile, err := os.Create(path)
|
keyFile, err := os.Create(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to create key file: " + err.Error())
|
return fmt.Errorf("failed to create key file: %w", err)
|
||||||
}
|
}
|
||||||
defer keyFile.Close()
|
defer keyFile.Close()
|
||||||
|
|
||||||
@@ -292,7 +274,7 @@ func (s *JwtService) savePEMKey(path string, keyBytes []byte, keyType string) er
|
|||||||
})
|
})
|
||||||
|
|
||||||
if _, err := keyFile.Write(keyPEM); err != nil {
|
if _, err := keyFile.Write(keyPEM); err != nil {
|
||||||
return errors.New("failed to write key file: " + err.Error())
|
return fmt.Errorf("failed to write key file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -336,8 +336,7 @@ wbeF6l05LexCkI7ShsOuSt+dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI=
|
|||||||
block, _ := pem.Decode([]byte(privateKeyString))
|
block, _ := pem.Decode([]byte(privateKeyString))
|
||||||
privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
|
privateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
|
||||||
s.jwtService.PrivateKey = privateKey
|
s.jwtService.SetKey(privateKey)
|
||||||
s.jwtService.PublicKey = &privateKey.PublicKey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key
|
// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key
|
||||||
|
|||||||
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.38.0",
|
"version": "0.39.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.38.0",
|
"version": "0.39.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
@@ -77,9 +77,10 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.26.7",
|
"version": "7.26.10",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
|
||||||
"integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
|
"integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
|
||||||
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pocket-id-frontend",
|
"name": "pocket-id-frontend",
|
||||||
"version": "0.39.0",
|
"version": "0.40.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ increment_version() {
|
|||||||
local part=$2
|
local part=$2
|
||||||
|
|
||||||
IFS='.' read -r -a parts <<<"$version"
|
IFS='.' read -r -a parts <<<"$version"
|
||||||
if [ "$part" == "minor" ]; then
|
if [ "$part" == "major" ]; then
|
||||||
|
parts[0]=$((parts[0] + 1))
|
||||||
|
parts[1]=0
|
||||||
|
parts[2]=0
|
||||||
|
elif [ "$part" == "minor" ]; then
|
||||||
parts[1]=$((parts[1] + 1))
|
parts[1]=$((parts[1] + 1))
|
||||||
parts[2]=0
|
parts[2]=0
|
||||||
elif [ "$part" == "patch" ]; then
|
elif [ "$part" == "patch" ]; then
|
||||||
@@ -22,16 +26,36 @@ increment_version() {
|
|||||||
echo "${parts[0]}.${parts[1]}.${parts[2]}"
|
echo "${parts[0]}.${parts[1]}.${parts[2]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
RELEASE_TYPE=$1
|
# Determine the release type
|
||||||
|
if [ "$1" == "major" ]; then
|
||||||
|
RELEASE_TYPE="major"
|
||||||
|
else
|
||||||
|
# Get the latest tag
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0)
|
||||||
|
|
||||||
if [ "$RELEASE_TYPE" == "minor" ]; then
|
# Check for "feat" or "fix" in the commit messages since the latest tag
|
||||||
|
if git log "$LATEST_TAG"..HEAD --oneline | grep -q "feat"; then
|
||||||
|
RELEASE_TYPE="minor"
|
||||||
|
elif git log "$LATEST_TAG"..HEAD --oneline | grep -q "fix"; then
|
||||||
|
RELEASE_TYPE="patch"
|
||||||
|
else
|
||||||
|
echo "No 'fix' or 'feat' commits found since the latest release. No new release will be created."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increment the version based on the release type
|
||||||
|
if [ "$RELEASE_TYPE" == "major" ]; then
|
||||||
|
echo "Performing major release..."
|
||||||
|
NEW_VERSION=$(increment_version $VERSION major)
|
||||||
|
elif [ "$RELEASE_TYPE" == "minor" ]; then
|
||||||
echo "Performing minor release..."
|
echo "Performing minor release..."
|
||||||
NEW_VERSION=$(increment_version $VERSION minor)
|
NEW_VERSION=$(increment_version $VERSION minor)
|
||||||
elif [ "$RELEASE_TYPE" == "patch" ]; then
|
elif [ "$RELEASE_TYPE" == "patch" ]; then
|
||||||
echo "Performing patch release..."
|
echo "Performing patch release..."
|
||||||
NEW_VERSION=$(increment_version $VERSION patch)
|
NEW_VERSION=$(increment_version $VERSION patch)
|
||||||
else
|
else
|
||||||
echo "Invalid release type. Please enter either 'minor' or 'patch'."
|
echo "Invalid release type. Please enter either 'major', 'minor', or 'patch'."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user