mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-07 10:14:13 -04:00
Add initial auth module (for v5) (#610)
This commit is contained in:
commit
c3b4f64941
@ -47,7 +47,7 @@ TVDB_PIN=
|
|||||||
|
|
||||||
|
|
||||||
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
||||||
PUBLIC_URL=http://localhost:5000
|
PUBLIC_URL=http://localhost:8901
|
||||||
|
|
||||||
# Use a builtin oidc service (google, discord, trakt, or simkl):
|
# Use a builtin oidc service (google, discord, trakt, or simkl):
|
||||||
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
|
# When you create a client_id, secret combo you may be asked for a redirect url. You need to specify https://YOUR-PUBLIC-URL/api/auth/logged/YOUR-SERVICE-NAME
|
||||||
|
20
.github/workflows/robot.yml
vendored
20
.github/workflows/robot.yml
vendored
@ -14,11 +14,13 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Pull images
|
- name: Robot cache
|
||||||
run: |
|
uses: actions/setup-python@v4
|
||||||
cp .env.example .env
|
with:
|
||||||
docker compose version
|
python-version: '3.9'
|
||||||
docker compose pull
|
cache: 'pip'
|
||||||
|
|
||||||
|
- run: pip install -r requirements.txt
|
||||||
|
|
||||||
- name: Docker cache
|
- name: Docker cache
|
||||||
uses: satackey/action-docker-layer-caching@v0.0.11
|
uses: satackey/action-docker-layer-caching@v0.0.11
|
||||||
@ -26,18 +28,18 @@ jobs:
|
|||||||
|
|
||||||
- name: Start the service
|
- name: Start the service
|
||||||
run: |
|
run: |
|
||||||
docker compose up -d back postgres traefik meilisearch --wait
|
cp .env.example .env
|
||||||
|
docker compose --profile v5 -f docker-compose.build.yml up -d auth postgres traefik --wait --build
|
||||||
|
|
||||||
- name: Perform healthchecks
|
- name: Perform healthchecks
|
||||||
run: |
|
run: |
|
||||||
docker compose ps -a
|
docker compose ps -a
|
||||||
docker compose logs
|
docker compose logs
|
||||||
wget --retry-connrefused --retry-on-http-error=502 http://localhost:8901/api/health || (docker compose logs && exit 1)
|
# wget --retry-connrefused --retry-on-http-error=502 http://localhost:8901/api/health || (docker compose logs && exit 1)
|
||||||
|
|
||||||
- name: Run robot tests
|
- name: Run robot tests
|
||||||
run: |
|
run: |
|
||||||
pip install -r back/tests/robot/requirements.txt
|
robot -d out $(find -type d -name robot)
|
||||||
robot -d out back/tests/robot/
|
|
||||||
|
|
||||||
- name: Show logs
|
- name: Show logs
|
||||||
if: failure()
|
if: failure()
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,4 +7,5 @@ log.html
|
|||||||
output.xml
|
output.xml
|
||||||
report.html
|
report.html
|
||||||
chart/charts
|
chart/charts
|
||||||
chart/Chart.lock
|
chart/Chart.lock
|
||||||
|
tmp
|
||||||
|
12
auth/.dockerignore
Normal file
12
auth/.dockerignore
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Dockerfile*
|
||||||
|
*.md
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# generated via sqlc
|
||||||
|
dbc/
|
||||||
|
# genereated via swag
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# vim: ft=gitignore
|
13
auth/.env.example
Normal file
13
auth/.env.example
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# vi: ft=sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
|
# Database things
|
||||||
|
POSTGRES_USER=
|
||||||
|
POSTGRES_PASSWORD=
|
||||||
|
POSTGRES_DB=
|
||||||
|
POSTGRES_SERVER=
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
# Default is keibi, you can specify "disabled" to use the default search_path of the user.
|
||||||
|
# If this is not "disabled", the schema will be created (if it does not exists) and
|
||||||
|
# the search_path of the user will be ignored (only the schema specified will be used).
|
||||||
|
POSTGRES_SCHEMA=keibi
|
4
auth/.gitignore
vendored
Normal file
4
auth/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# generated via sqlc
|
||||||
|
dbc/
|
||||||
|
# genereated via swag
|
||||||
|
docs/
|
2
auth/.swaggo
Normal file
2
auth/.swaggo
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
replace jwt.MapClaims map[string]string
|
||||||
|
replace uuid.UUID string
|
25
auth/Dockerfile
Normal file
25
auth/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
FROM golang:1.23 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||||
|
RUN go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY sqlc.yaml ./
|
||||||
|
COPY sql ./sql
|
||||||
|
RUN sqlc generate
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN swag init --parseDependency
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /keibi
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/base-debian11
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 4568
|
||||||
|
USER nonroot:nonroot
|
||||||
|
|
||||||
|
COPY --from=build /keibi /app/keibi
|
||||||
|
COPY sql ./sql
|
||||||
|
CMD ["/app/keibi"]
|
16
auth/Dockerfile.dev
Normal file
16
auth/Dockerfile.dev
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM golang:1.23 AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN go install github.com/bokwoon95/wgo@latest
|
||||||
|
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||||
|
RUN go install github.com/swaggo/swag/cmd/swag@latest
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# COPY sqlc.yaml ./
|
||||||
|
# COPY sql/ ./
|
||||||
|
# RUN sqlc generate
|
||||||
|
|
||||||
|
EXPOSE 4568
|
||||||
|
CMD ["wgo", "run", "-race", "."]
|
151
auth/README.md
Normal file
151
auth/README.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Keibi
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Not an oauth provider/no login page (as in you don't redirect to this, you create your own auth page)
|
||||||
|
- [Phantom tokens](https://curity.io/resources/learn/phantom-token-pattern/)
|
||||||
|
- Session based tokens (valid for 30 days, reset after each use [configurable])
|
||||||
|
- Last online/last connection stored per user (and token)
|
||||||
|
- Device used per session/token
|
||||||
|
- Username/password login
|
||||||
|
- OIDC (login via Google, Discord, Authentik, whatever)
|
||||||
|
- Custom jwt claims (for your role/permissions handling or something else)
|
||||||
|
- Api keys support
|
||||||
|
- Optionally [Federated](#federated)
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
### Lifecycle
|
||||||
|
|
||||||
|
|
||||||
|
Login:
|
||||||
|
|
||||||
|
`POST /session { login, password } -> token`
|
||||||
|
`GET /login/$provider { redirectUrl, tenant? } -> redirect`
|
||||||
|
|
||||||
|
Register:
|
||||||
|
`POST /users { email, username, password } -> token`
|
||||||
|
|
||||||
|
Logout
|
||||||
|
`DELETE /session` w/ optional `?session=id`
|
||||||
|
`/jwt` retrieve a jwt from an opaque token (also update last online value for session & user)
|
||||||
|
|
||||||
|
### Profiles
|
||||||
|
|
||||||
|
```
|
||||||
|
Get `/users` -> user[]
|
||||||
|
Get/Put/Patch/Delete `/users/$id` (or /users/me) -> user
|
||||||
|
Get/Post/Delete `/users/$id/logo` (or /users/me/logo) -> png
|
||||||
|
```
|
||||||
|
|
||||||
|
Put/Patch of a user can edit the password if the `oldPassword` value is set and valid (or the user has the `users.password` permission).\
|
||||||
|
Should require an otp from mail if no oldPassword exists (see todo).
|
||||||
|
|
||||||
|
Put/Patch can edit custom claims (roles & permissons for example) if the user has the `users.claims` permission).
|
||||||
|
|
||||||
|
Read others requires `users.read` permission.\
|
||||||
|
Write/Delete requires `users.write` permission (if it's not your account).
|
||||||
|
|
||||||
|
|
||||||
|
POST /users is how you register.
|
||||||
|
|
||||||
|
### Sessions
|
||||||
|
|
||||||
|
GET `/sessions` list all of your active sessions (and devices)
|
||||||
|
POST `/sessions` is how you login
|
||||||
|
Delete `/sessions` (or `/sessions/$id`) is how you logout
|
||||||
|
GET `/users/$id/sessions` can be used by admins to list others session
|
||||||
|
|
||||||
|
### Api keys
|
||||||
|
|
||||||
|
```
|
||||||
|
Get `/apikeys`
|
||||||
|
Post `/apikeys` {...nlaims} Create a new api keys with given claims
|
||||||
|
```
|
||||||
|
|
||||||
|
An api key can be used like an opaque token, calling /jwt with it will return a valid jwt with the claims you specified during the post request to create it.
|
||||||
|
Creating an apikeys requires the `apikey.create` permission, reading them requires the `apikey.read` permission.
|
||||||
|
|
||||||
|
### OIDC
|
||||||
|
|
||||||
|
```
|
||||||
|
`/login/$provider` {redirectUrl, tenant?}
|
||||||
|
`/logged/$provider` {code, state, error} (callback called automatically, don't call it manually)
|
||||||
|
`/callback/$provider` {code, tenant?} (if called with the `Authorization` header, links account w/ provider else create a new account) (see diagram below)
|
||||||
|
`/unlink/$provider` Remove provider from current account
|
||||||
|
`/providers` -> provider[]
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App
|
||||||
|
participant Browser
|
||||||
|
participant Kyoo
|
||||||
|
participant Google
|
||||||
|
App->>Kyoo: /login/google?redirectUrl=/user-logged
|
||||||
|
Kyoo->>Browser: redirect auth.google.com?state=id=guid,url=/user-logged&redirectUrl=/logged/google
|
||||||
|
Browser-->>Google: Access login page
|
||||||
|
Google->>Browser: redirect /logged/google?code=abc&state=id=guid,url=/user-logged
|
||||||
|
Browser-->>Kyoo: access /logged/google?code=abc&state=id=guid,url=/user-logged
|
||||||
|
Kyoo->>App: redirect /user-logged?token=opaque&error=
|
||||||
|
App->>Kyoo: /callback/google?token=opaque
|
||||||
|
Kyoo->>Google: auth.google.com/token?code=abc
|
||||||
|
Google->>Kyoo: jwt token
|
||||||
|
Kyoo->>Google: auth.google.com/profile (w/ jwt)
|
||||||
|
Google->>Kyoo: profile info
|
||||||
|
Kyoo->>App: Token if user exist/was created
|
||||||
|
```
|
||||||
|
|
||||||
|
In the previous diagram, the code is stored by Kyoo and an opaque token is returned to the client to ensure only Kyoo's auth service can read the oauth code.
|
||||||
|
|
||||||
|
## Federated
|
||||||
|
|
||||||
|
You can use another instance to login via oidc you have not configured. This allows an user to login/create a profile without having an api key for the oidc service.
|
||||||
|
This won't allow you to retrive a provider's jwt token, you only get a profile with basic information from the provider. This can be usefull for self-hosted apps where
|
||||||
|
you don't want to setup ~10 api keys just for login.
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant App
|
||||||
|
participant Browser
|
||||||
|
participant Kyoo
|
||||||
|
participant Hosted
|
||||||
|
participant Google
|
||||||
|
App->>Kyoo: /login/google?redirectUrl=/user-logged
|
||||||
|
Kyoo->>Hosted: /providers
|
||||||
|
Hosted->>Kyoo: has google = true
|
||||||
|
Kyoo->>Browser: redirect hosted.com/login/google?redirectUrl=/user-logged&tenant=kyoo.com
|
||||||
|
Browser-->>Hosted: access /login/google?redirectUrl=/user-logged&tenant=kyoo.com
|
||||||
|
Hosted->>Browser: redirect auth.google.com?state=id=guid,url=/user-logged,tenant=kyoo.com&redirectUrl=/logged/google
|
||||||
|
Browser-->>Google: Access login page
|
||||||
|
Google->>Browser: redirect hosted.com/logged/google?code=abc&state=id=guid,url=/user-logged,tenant=kyoo.com
|
||||||
|
Browser-->>Hosted: access /logged/google?code=abc&state=id=guid,url=/user-logged,tenant=kyoo.com
|
||||||
|
Hosted->>App: redirect /user-logged?token=opaque&error=
|
||||||
|
App->>Kyoo: /callback/google?token=opaque
|
||||||
|
Kyoo->>Hosted: /callback/google?token=opaque&tenant=kyoo.com
|
||||||
|
Hosted->>Google: auth.google.com/token?code=abc
|
||||||
|
Google->>Hosted: jwt token
|
||||||
|
Hosted->>Google: auth.google.com/profile (w/ jwt)
|
||||||
|
Google->>Hosted: profile info
|
||||||
|
Hosted->>Kyoo: profile info (without a jwt to access the provider)
|
||||||
|
Kyoo->>App: Token if user exist/was created
|
||||||
|
```
|
||||||
|
|
||||||
|
The hosted service does not store any user data during this interaction.
|
||||||
|
A `/login` requests temporally stores an id, the tenant & the redirectUrl to unsure the profile value is not stollen. This is then deleted after a `/callback` call (or on timeout).
|
||||||
|
User profile or jwt is never stored.
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
You might have noticed that some routes requires the user to have some permissions.
|
||||||
|
Kyoo's auth uses the custom `permissions` claim for this.
|
||||||
|
Your application is free to use this or any other way of handling permissions/roles.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Reset/forget password
|
||||||
|
- Login via qrcode/code from other device (useful for tv for example)
|
||||||
|
- LDMA?
|
||||||
|
- Mails
|
||||||
|
|
79
auth/config.go
Normal file
79
auth/config.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/zoriya/kyoo/keibi/dbc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Configuration struct {
|
||||||
|
JwtPrivateKey *rsa.PrivateKey
|
||||||
|
JwtPublicKey *rsa.PublicKey
|
||||||
|
Issuer string
|
||||||
|
DefaultClaims jwt.MapClaims
|
||||||
|
ExpirationDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultConfig = Configuration{
|
||||||
|
Issuer: "kyoo",
|
||||||
|
DefaultClaims: make(jwt.MapClaims),
|
||||||
|
ExpirationDelay: 30 * 24 * time.Hour,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
JwtPrivateKey = "jwt_private_key"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
confs, err := db.LoadConfig(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := DefaultConfig
|
||||||
|
|
||||||
|
for _, conf := range confs {
|
||||||
|
switch conf.Key {
|
||||||
|
case JwtPrivateKey:
|
||||||
|
block, _ := pem.Decode([]byte(conf.Value))
|
||||||
|
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret.JwtPrivateKey = key
|
||||||
|
ret.JwtPublicKey = &key.PublicKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret.JwtPrivateKey == nil {
|
||||||
|
ret.JwtPrivateKey, err = rsa.GenerateKey(rand.Reader, 4096)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ret.JwtPublicKey = &ret.JwtPrivateKey.PublicKey
|
||||||
|
|
||||||
|
pemd := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PrivateKey(ret.JwtPrivateKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := db.SaveConfig(ctx, dbc.SaveConfigParams{
|
||||||
|
Key: JwtPrivateKey,
|
||||||
|
Value: string(pemd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ret, nil
|
||||||
|
}
|
62
auth/go.mod
Normal file
62
auth/go.mod
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
module github.com/zoriya/kyoo/keibi
|
||||||
|
|
||||||
|
go 1.22.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0
|
||||||
|
github.com/labstack/echo/v4 v4.12.0
|
||||||
|
github.com/otaxhu/problem v0.2.0
|
||||||
|
github.com/swaggo/echo-swagger v1.4.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||||
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
|
github.com/docker/docker v27.1.2+incompatible // indirect
|
||||||
|
github.com/docker/go-connections v0.5.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/ghodss/yaml v1.0.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.22.0
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/labstack/echo-jwt/v4 v4.2.0
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||||
|
github.com/swaggo/files/v2 v2.0.1 // indirect
|
||||||
|
github.com/swaggo/swag v1.16.3
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
golang.org/x/crypto v0.26.0 // indirect
|
||||||
|
golang.org/x/net v0.28.0 // indirect
|
||||||
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
|
golang.org/x/sys v0.24.0 // indirect
|
||||||
|
golang.org/x/text v0.17.0 // indirect
|
||||||
|
golang.org/x/time v0.5.0 // indirect
|
||||||
|
golang.org/x/tools v0.24.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
228
auth/go.sum
Normal file
228
auth/go.sum
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||||
|
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||||
|
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||||
|
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||||
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
|
github.com/PuerkitoBio/purell v1.2.1 h1:QsZ4TjvwiMpat6gBCBxEQI0rcS9ehtkKtSpiUnd9N28=
|
||||||
|
github.com/PuerkitoBio/purell v1.2.1/go.mod h1:ZwHcC/82TOaovDi//J/804umJFFmbOHPngi8iYYv/Eo=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||||
|
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||||
|
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
|
||||||
|
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
|
github.com/docker/docker v27.1.2+incompatible h1:AhGzR1xaQIy53qCkxARaFluI00WPGtXn0AJuoQsVYTY=
|
||||||
|
github.com/docker/docker v27.1.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA=
|
||||||
|
github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
|
||||||
|
github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
|
||||||
|
github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
|
github.com/golang-migrate/migrate v3.5.4+incompatible h1:R7OzwvCJTCgwapPCiX6DyBiu2czIUMDCB118gFTKTUA=
|
||||||
|
github.com/golang-migrate/migrate v3.5.4+incompatible/go.mod h1:IsVUlFN5puWOmXrqjgGUfIRIbU7mr8oNBE2tyERd9Wk=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0=
|
||||||
|
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||||
|
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
|
||||||
|
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
|
||||||
|
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
|
||||||
|
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||||
|
github.com/otaxhu/problem v0.2.0 h1:cxVSlWHPi0zn1Mvl3/SVwySnnxfpHslENU1MvouSEME=
|
||||||
|
github.com/otaxhu/problem v0.2.0/go.mod h1:bp1KCPkRRBORbIg4a/p/Sa+FuFuMHVg+iEjnWL/LMKA=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
|
||||||
|
github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
|
||||||
|
github.com/swaggo/files/v2 v2.0.1 h1:XCVJO/i/VosCDsJu1YLpdejGsGnBE9deRMpjN4pJLHk=
|
||||||
|
github.com/swaggo/files/v2 v2.0.1/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||||
|
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||||
|
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||||
|
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||||
|
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||||
|
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||||
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
|
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||||
|
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||||
|
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||||
|
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
|
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
|
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||||
|
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
|
||||||
|
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
92
auth/jwt.go
Normal file
92
auth/jwt.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"maps"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Jwt struct {
|
||||||
|
// The jwt token you can use for all authorized call to either keibi or other services.
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
// The public key used to sign jwt tokens. It can be used by your services to check if the jwt is valid.
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get JWT
|
||||||
|
// @Description Convert a session token to a short lived JWT.
|
||||||
|
// @Tags jwt
|
||||||
|
// @Produce json
|
||||||
|
// @Security Token
|
||||||
|
// @Success 200 {object} Jwt
|
||||||
|
// @Failure 401 {object} problem.Problem "Missing session token"
|
||||||
|
// @Failure 403 {object} problem.Problem "Invalid session token (or expired)"
|
||||||
|
// @Router /jwt [get]
|
||||||
|
func (h *Handler) CreateJwt(c echo.Context) error {
|
||||||
|
auth := c.Request().Header.Get("Authorization")
|
||||||
|
if !strings.HasPrefix(auth, "Bearer ") {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing session token")
|
||||||
|
}
|
||||||
|
token := auth[len("Bearer "):]
|
||||||
|
|
||||||
|
session, err := h.db.GetUserFromToken(context.Background(), token)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, "Invalid token")
|
||||||
|
}
|
||||||
|
if session.LastUsed.Add(h.config.ExpirationDelay).Compare(time.Now().UTC()) < 0 {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, "Token has expired")
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
h.db.TouchSession(context.Background(), session.Id)
|
||||||
|
h.db.TouchUser(context.Background(), session.User.Id)
|
||||||
|
}()
|
||||||
|
|
||||||
|
claims := maps.Clone(session.User.Claims)
|
||||||
|
claims["sub"] = session.User.Id.String()
|
||||||
|
claims["sid"] = session.Id.String()
|
||||||
|
claims["iss"] = h.config.Issuer
|
||||||
|
claims["exp"] = &jwt.NumericDate{
|
||||||
|
Time: time.Now().UTC().Add(time.Hour),
|
||||||
|
}
|
||||||
|
claims["iss"] = &jwt.NumericDate{
|
||||||
|
Time: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||||
|
t, err := jwt.SignedString(h.config.JwtPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, Jwt{
|
||||||
|
Token: t,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Info
|
||||||
|
// @Description Get info like the public key used to sign the jwts.
|
||||||
|
// @Tags jwt
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} Info
|
||||||
|
// @Router /info [get]
|
||||||
|
func (h *Handler) GetInfo(c echo.Context) error {
|
||||||
|
key := pem.EncodeToMemory(
|
||||||
|
&pem.Block{
|
||||||
|
Type: "RSA PUBLIC KEY",
|
||||||
|
Bytes: x509.MarshalPKCS1PublicKey(h.config.JwtPublicKey),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return c.JSON(200, Info{
|
||||||
|
PublicKey: string(key),
|
||||||
|
})
|
||||||
|
}
|
183
auth/main.go
Normal file
183
auth/main.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/otaxhu/problem"
|
||||||
|
"github.com/zoriya/kyoo/keibi/dbc"
|
||||||
|
_ "github.com/zoriya/kyoo/keibi/docs"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
pgxd "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||||
|
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/jackc/pgx/v5/stdlib"
|
||||||
|
"github.com/labstack/echo-jwt/v4"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
"github.com/swaggo/echo-swagger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ErrorHandler(err error, c echo.Context) {
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
var message string
|
||||||
|
|
||||||
|
if he, ok := err.(*echo.HTTPError); ok {
|
||||||
|
code = he.Code
|
||||||
|
message = fmt.Sprint(he.Message)
|
||||||
|
} else {
|
||||||
|
c.Logger().Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := problem.NewDefault(code)
|
||||||
|
if message != "" {
|
||||||
|
ret.Detail = message
|
||||||
|
}
|
||||||
|
c.JSON(code, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Validator struct {
|
||||||
|
validator *validator.Validate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Validator) Validate(i interface{}) error {
|
||||||
|
if err := v.validator.Struct(i); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenDatabase() (*pgxpool.Pool, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
port, err := strconv.ParseUint(os.Getenv("POSTGRES_PORT"), 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid postgres port specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _ := pgxpool.ParseConfig("")
|
||||||
|
config.ConnConfig.Host = os.Getenv("POSTGRES_SERVER")
|
||||||
|
config.ConnConfig.Port = uint16(port)
|
||||||
|
config.ConnConfig.Database = os.Getenv("POSTGRES_DB")
|
||||||
|
config.ConnConfig.User = os.Getenv("POSTGRES_USER")
|
||||||
|
config.ConnConfig.Password = os.Getenv("POSTGRES_PASSWORD")
|
||||||
|
config.ConnConfig.TLSConfig = nil
|
||||||
|
config.ConnConfig.RuntimeParams = map[string]string{
|
||||||
|
"application_name": "keibi",
|
||||||
|
}
|
||||||
|
schema := os.Getenv("POSTGRES_SCHEMA")
|
||||||
|
if schema == "" {
|
||||||
|
schema = "keibi"
|
||||||
|
}
|
||||||
|
if schema != "disabled" {
|
||||||
|
config.ConnConfig.RuntimeParams["search_path"] = schema
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := pgxpool.NewWithConfig(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Could not connect to database, check your env variables!")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if schema != "disabled" {
|
||||||
|
_, err = db.Exec(ctx, fmt.Sprintf("create schema if not exists %s", schema))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Migrating database")
|
||||||
|
dbi := stdlib.OpenDBFromPool(db)
|
||||||
|
defer dbi.Close()
|
||||||
|
|
||||||
|
driver, err := pgxd.WithInstance(dbi, &pgxd.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m, err := migrate.NewWithDatabaseInstance("file://sql/migrations", "postgres", driver)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m.Up()
|
||||||
|
fmt.Println("Migrating finished")
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
db *dbc.Queries
|
||||||
|
config *Configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// @title Keibi - Kyoo's auth
|
||||||
|
// @version 1.0
|
||||||
|
// @description Auth system made for kyoo.
|
||||||
|
|
||||||
|
// @contact.name Repository
|
||||||
|
// @contact.url https://github.com/zoriya/kyoo
|
||||||
|
|
||||||
|
// @license.name GPL-3.0
|
||||||
|
// @license.url https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||||
|
|
||||||
|
// @host kyoo.zoriya.dev
|
||||||
|
// @BasePath /auth
|
||||||
|
|
||||||
|
// @securityDefinitions.apiKey Token
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
|
||||||
|
// @securityDefinitions.apiKey Jwt
|
||||||
|
// @in header
|
||||||
|
// @name Authorization
|
||||||
|
func main() {
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Validator = &Validator{validator: validator.New(validator.WithRequiredStructEnabled())}
|
||||||
|
e.HTTPErrorHandler = ErrorHandler
|
||||||
|
|
||||||
|
db, err := OpenDatabase()
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Fatal("Could not open databse: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h := Handler{
|
||||||
|
db: dbc.New(db),
|
||||||
|
}
|
||||||
|
conf, err := LoadConfiguration(h.db)
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Fatal("Could not load configuration: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.config = conf
|
||||||
|
|
||||||
|
r := e.Group("")
|
||||||
|
r.Use(echojwt.WithConfig(echojwt.Config{
|
||||||
|
SigningMethod: "RS256",
|
||||||
|
SigningKey: h.config.JwtPublicKey,
|
||||||
|
}))
|
||||||
|
|
||||||
|
r.GET("/users", h.ListUsers)
|
||||||
|
r.GET("/users/:id", h.GetUser)
|
||||||
|
r.GET("/users/me", h.GetMe)
|
||||||
|
r.DELETE("/users/:id", h.DeleteUser)
|
||||||
|
r.DELETE("/users/me", h.DeleteSelf)
|
||||||
|
e.POST("/users", h.Register)
|
||||||
|
|
||||||
|
e.POST("/sessions", h.Login)
|
||||||
|
r.DELETE("/sessions", h.Logout)
|
||||||
|
r.DELETE("/sessions/:id", h.Logout)
|
||||||
|
|
||||||
|
e.GET("/jwt", h.CreateJwt)
|
||||||
|
e.GET("/info", h.GetInfo)
|
||||||
|
|
||||||
|
e.GET("/swagger/*", echoSwagger.WrapHandler)
|
||||||
|
|
||||||
|
e.Logger.Fatal(e.Start(":4568"))
|
||||||
|
}
|
43
auth/robot/auth.resource
Normal file
43
auth/robot/auth.resource
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Common things to handle rest requests
|
||||||
|
|
||||||
|
Library REST http://localhost:8901/auth
|
||||||
|
|
||||||
|
|
||||||
|
*** Keywords ***
|
||||||
|
Login
|
||||||
|
[Documentation] Shortcut to login with the given username for future requests
|
||||||
|
[Arguments] ${username}
|
||||||
|
&{res}= POST /sessions {"login": "${username}", "password": "password-${username}"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
String response body token
|
||||||
|
ConvertToJwt ${res.body.token}
|
||||||
|
|
||||||
|
Register
|
||||||
|
[Documentation] Shortcut to register with the given username for future requests
|
||||||
|
[Arguments] ${username}
|
||||||
|
&{res}= POST
|
||||||
|
... /users
|
||||||
|
... {"username": "${username}", "password": "password-${username}", "email": "${username}@zoriya.dev"}
|
||||||
|
Output
|
||||||
|
Integer response status 201
|
||||||
|
String response body token
|
||||||
|
ConvertToJwt ${res.body.token}
|
||||||
|
|
||||||
|
ConvertToJwt
|
||||||
|
[Documentation] Convert a session token to a jwt and set it in the header
|
||||||
|
[Arguments] ${token}
|
||||||
|
Set Headers {"Authorization": "Bearer ${token}"}
|
||||||
|
&{res}= GET /jwt
|
||||||
|
Output
|
||||||
|
Integer response status 200
|
||||||
|
String response body token
|
||||||
|
Set Headers {"Authorization": "Bearer ${res.body.token}"}
|
||||||
|
|
||||||
|
Logout
|
||||||
|
[Documentation] Logout the current user, only the local client is affected.
|
||||||
|
${res}= DELETE /sessions/current
|
||||||
|
Output
|
||||||
|
Integer response status 200
|
||||||
|
Set Headers {"Authorization": ""}
|
36
auth/robot/sessions.robot
Normal file
36
auth/robot/sessions.robot
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Tests of the /sessions route.
|
||||||
|
|
||||||
|
Resource ./auth.resource
|
||||||
|
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Bad Account
|
||||||
|
[Documentation] Login fails if user does not exist
|
||||||
|
POST /sessions {"login": "i-don-t-exist", "password": "pass"}
|
||||||
|
Output
|
||||||
|
Integer response status 404
|
||||||
|
|
||||||
|
Invalid password
|
||||||
|
[Documentation] Login fails if password is invalid
|
||||||
|
Register invalid-password-user
|
||||||
|
POST /sessions {"login": "invalid-password-user", "password": "pass"}
|
||||||
|
Output
|
||||||
|
Integer response status 403
|
||||||
|
[Teardown] DELETE /users/me
|
||||||
|
|
||||||
|
Login
|
||||||
|
[Documentation] Create a new user and login in it
|
||||||
|
Register login-user
|
||||||
|
${res}= GET /users/me
|
||||||
|
Output
|
||||||
|
Integer response status 200
|
||||||
|
String response body username login-user
|
||||||
|
Logout
|
||||||
|
Login login-user
|
||||||
|
${me}= Get /users/me
|
||||||
|
Output
|
||||||
|
Output ${me}
|
||||||
|
Should Be Equal As Strings ${res["body"]} ${me["body"]}
|
||||||
|
|
||||||
|
[Teardown] DELETE /users/me
|
33
auth/robot/users.robot
Normal file
33
auth/robot/users.robot
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Tests of the /users route.
|
||||||
|
... Ensures that the user can authenticate on kyoo.
|
||||||
|
|
||||||
|
Resource ./auth.resource
|
||||||
|
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Me cant be accessed without an account
|
||||||
|
Get /users/me
|
||||||
|
Output
|
||||||
|
Integer response status 401
|
||||||
|
|
||||||
|
Register
|
||||||
|
[Documentation] Create a new user and login in it
|
||||||
|
Register user-1
|
||||||
|
[Teardown] DELETE /users/me
|
||||||
|
|
||||||
|
Register Duplicates
|
||||||
|
[Documentation] If two users tries to register with the same username, it fails
|
||||||
|
Register user-duplicate
|
||||||
|
# We can't use the `Register` keyword because it assert for success
|
||||||
|
POST /users {"username": "user-duplicate", "password": "pass", "email": "mail@zoriya.dev"}
|
||||||
|
Output
|
||||||
|
Integer response status 409
|
||||||
|
[Teardown] DELETE /users/me
|
||||||
|
|
||||||
|
Delete Account
|
||||||
|
[Documentation] Check if a user can delete it's account
|
||||||
|
Register I-should-be-deleted
|
||||||
|
DELETE /users/me
|
||||||
|
Output
|
||||||
|
Integer response status 200
|
157
auth/sessions.go
Normal file
157
auth/sessions.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/zoriya/kyoo/keibi/dbc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
// Unique id of this session. Can be used for calls to DELETE
|
||||||
|
Id uuid.UUID `json:"id"`
|
||||||
|
// When was the session first opened
|
||||||
|
CreatedDate time.Time `json:"createdDate"`
|
||||||
|
// Last date this session was used to access a service.
|
||||||
|
LastUsed time.Time `json:"lastUsed"`
|
||||||
|
// Device that created the session.
|
||||||
|
Device *string `json:"device"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapSession(ses *dbc.Session) Session {
|
||||||
|
return Session{
|
||||||
|
Id: ses.Id,
|
||||||
|
CreatedDate: ses.CreatedDate,
|
||||||
|
LastUsed: ses.LastUsed,
|
||||||
|
Device: ses.Device,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginDto struct {
|
||||||
|
// Either the email or the username.
|
||||||
|
Login string `json:"login" validate:"required"`
|
||||||
|
// Password of the account.
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Login
|
||||||
|
// @Description Login to your account and open a session
|
||||||
|
// @Tags sessions
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param device query string false "The device the created session will be used on"
|
||||||
|
// @Param login body LoginDto false "Account informations"
|
||||||
|
// @Success 201 {object} dbc.Session
|
||||||
|
// @Failure 400 {object} problem.Problem "Invalid login body"
|
||||||
|
// @Failure 403 {object} problem.Problem "Invalid password"
|
||||||
|
// @Failure 404 {object} problem.Problem "Account does not exists"
|
||||||
|
// @Failure 422 {object} problem.Problem "User does not have a password (registered via oidc, please login via oidc)"
|
||||||
|
// @Router /sessions [post]
|
||||||
|
func (h *Handler) Login(c echo.Context) error {
|
||||||
|
var req LoginDto
|
||||||
|
err := c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if err = c.Validate(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dbuser, err := h.db.GetUserByLogin(context.Background(), req.Login)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "No account exists with the specified email or username.")
|
||||||
|
}
|
||||||
|
if dbuser.Password == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnprocessableEntity, "Can't login with password, this account was created with OIDC.")
|
||||||
|
}
|
||||||
|
|
||||||
|
match, err := argon2id.ComparePasswordAndHash(req.Password, *dbuser.Password)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, "Invalid password")
|
||||||
|
}
|
||||||
|
|
||||||
|
user := MapDbUser(&dbuser)
|
||||||
|
return h.createSession(c, &user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) createSession(c echo.Context, user *User) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
id := make([]byte, 64)
|
||||||
|
_, err := rand.Read(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dev := cmp.Or(c.Param("device"), c.Request().Header.Get("User-Agent"))
|
||||||
|
device := &dev
|
||||||
|
if dev == "" {
|
||||||
|
device = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := h.db.CreateSession(ctx, dbc.CreateSessionParams{
|
||||||
|
Token: base64.StdEncoding.EncodeToString(id),
|
||||||
|
UserPk: user.Pk,
|
||||||
|
Device: device,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(201, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Logout
|
||||||
|
// @Description Delete a session and logout
|
||||||
|
// @Tags sessions
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt
|
||||||
|
// @Param id path string true "The id of the session to delete" Format(uuid)
|
||||||
|
// @Success 200 {object} Session
|
||||||
|
// @Failure 400 {object} problem.Problem "Invalid session id"
|
||||||
|
// @Failure 401 {object} problem.Problem "Missing jwt token"
|
||||||
|
// @Failure 403 {object} problem.Problem "Invalid jwt token (or expired)"
|
||||||
|
// @Failure 404 {object} problem.Problem "Session not found with specified id (if not using the /current route)"
|
||||||
|
// @Router /sessions/{id} [delete]
|
||||||
|
// @Router /sessions/current [delete]
|
||||||
|
func (h *Handler) Logout(c echo.Context) error {
|
||||||
|
uid, err := GetCurrentUserId(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
session := c.Param("id")
|
||||||
|
if session == "current" {
|
||||||
|
sid, ok := c.Get("user").(*jwt.Token).Claims.(jwt.MapClaims)["sid"]
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(400, "Missing session id")
|
||||||
|
}
|
||||||
|
session = sid.(string)
|
||||||
|
}
|
||||||
|
sid, err := uuid.Parse(session)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(400, "Invalid session id")
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := h.db.DeleteSession(context.Background(), dbc.DeleteSessionParams{
|
||||||
|
Id: sid,
|
||||||
|
UserId: uid,
|
||||||
|
})
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(404, "Session not found with specified id")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(200, MapSession(&ret))
|
||||||
|
}
|
6
auth/sql/migrations/000001_users.down.sql
Normal file
6
auth/sql/migrations/000001_users.down.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
drop table oidc_handle;
|
||||||
|
drop table users;
|
||||||
|
|
||||||
|
commit;
|
30
auth/sql/migrations/000001_users.up.sql
Normal file
30
auth/sql/migrations/000001_users.up.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
create table users(
|
||||||
|
pk serial primary key,
|
||||||
|
id uuid not null default gen_random_uuid(),
|
||||||
|
username varchar(256) not null unique,
|
||||||
|
email varchar(320) not null unique,
|
||||||
|
password text,
|
||||||
|
claims jsonb not null,
|
||||||
|
|
||||||
|
created_date timestamptz not null default now()::timestamptz,
|
||||||
|
last_seen timestamptz not null default now()::timestamptz
|
||||||
|
);
|
||||||
|
|
||||||
|
create table oidc_handle(
|
||||||
|
user_pk integer not null references users(pk) on delete cascade,
|
||||||
|
provider varchar(256) not null,
|
||||||
|
|
||||||
|
id text not null,
|
||||||
|
username varchar(256) not null,
|
||||||
|
profile_url text,
|
||||||
|
|
||||||
|
access_token text,
|
||||||
|
refresh_token text,
|
||||||
|
expire_at timestamptz,
|
||||||
|
|
||||||
|
constraint oidc_handle_pk primary key (user_pk, provider)
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
5
auth/sql/migrations/000002_config.down.sql
Normal file
5
auth/sql/migrations/000002_config.down.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
drop table config;
|
||||||
|
|
||||||
|
commit;
|
8
auth/sql/migrations/000002_config.up.sql
Normal file
8
auth/sql/migrations/000002_config.up.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
create table config(
|
||||||
|
key varchar(256) not null primary key,
|
||||||
|
value text not null
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
5
auth/sql/migrations/000003_sessions.down.sql
Normal file
5
auth/sql/migrations/000003_sessions.down.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
drop table sessions;
|
||||||
|
|
||||||
|
commit;
|
13
auth/sql/migrations/000003_sessions.up.sql
Normal file
13
auth/sql/migrations/000003_sessions.up.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
begin;
|
||||||
|
|
||||||
|
create table sessions(
|
||||||
|
pk serial primary key,
|
||||||
|
id uuid not null default gen_random_uuid(),
|
||||||
|
token varchar(128) not null unique,
|
||||||
|
user_pk integer not null references users(pk) on delete cascade,
|
||||||
|
created_date timestamptz not null default now()::timestamptz,
|
||||||
|
last_used timestamptz not null default now()::timestamptz,
|
||||||
|
device varchar(1024)
|
||||||
|
);
|
||||||
|
|
||||||
|
commit;
|
21
auth/sql/queries/config.sql
Normal file
21
auth/sql/queries/config.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-- name: LoadConfig :many
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
config;
|
||||||
|
|
||||||
|
-- name: SaveConfig :one
|
||||||
|
insert into config(key, value)
|
||||||
|
values ($1, $2)
|
||||||
|
on conflict (key)
|
||||||
|
do update set
|
||||||
|
value = excluded.value
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteConfig :one
|
||||||
|
delete from config
|
||||||
|
where key = $1
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
45
auth/sql/queries/sessions.sql
Normal file
45
auth/sql/queries/sessions.sql
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
-- name: GetUserFromToken :one
|
||||||
|
select
|
||||||
|
s.id,
|
||||||
|
s.last_used,
|
||||||
|
sqlc.embed(u)
|
||||||
|
from
|
||||||
|
users as u
|
||||||
|
inner join sessions as s on u.pk = s.user_pk
|
||||||
|
where
|
||||||
|
s.token = $1
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
-- name: TouchSession :exec
|
||||||
|
update
|
||||||
|
sessions
|
||||||
|
set
|
||||||
|
last_used = now()::timestamptz
|
||||||
|
where
|
||||||
|
id = $1;
|
||||||
|
|
||||||
|
-- name: GetUserSessions :many
|
||||||
|
select
|
||||||
|
s.*
|
||||||
|
from
|
||||||
|
sessions as s
|
||||||
|
inner join users as u on u.pk = s.user_pk
|
||||||
|
where
|
||||||
|
u.pk = $1
|
||||||
|
order by
|
||||||
|
last_used;
|
||||||
|
|
||||||
|
-- name: CreateSession :one
|
||||||
|
insert into sessions(token, user_pk, device)
|
||||||
|
values ($1, $2, $3)
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteSession :one
|
||||||
|
delete from sessions as s using users as u
|
||||||
|
where s.user_pk = u.pk
|
||||||
|
and s.id = $1
|
||||||
|
and u.id = sqlc.arg(user_id)
|
||||||
|
returning
|
||||||
|
s.*;
|
||||||
|
|
76
auth/sql/queries/users.sql
Normal file
76
auth/sql/queries/users.sql
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
-- name: GetAllUsers :many
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
users
|
||||||
|
order by
|
||||||
|
id
|
||||||
|
limit $1;
|
||||||
|
|
||||||
|
-- name: GetAllUsersAfter :many
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
users
|
||||||
|
where
|
||||||
|
id >= sqlc.arg(after_id)
|
||||||
|
order by
|
||||||
|
id
|
||||||
|
limit $1;
|
||||||
|
|
||||||
|
-- name: GetUser :many
|
||||||
|
select
|
||||||
|
sqlc.embed(u),
|
||||||
|
h.provider,
|
||||||
|
h.id,
|
||||||
|
h.username,
|
||||||
|
h.profile_url
|
||||||
|
from
|
||||||
|
users as u
|
||||||
|
left join oidc_handle as h on u.pk = h.user_pk
|
||||||
|
where
|
||||||
|
u.id = $1;
|
||||||
|
|
||||||
|
-- name: GetUserByLogin :one
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
users
|
||||||
|
where
|
||||||
|
email = sqlc.arg(login)
|
||||||
|
or username = sqlc.arg(login)
|
||||||
|
limit 1;
|
||||||
|
|
||||||
|
-- name: TouchUser :exec
|
||||||
|
update
|
||||||
|
users
|
||||||
|
set
|
||||||
|
last_used = now()::timestamptz
|
||||||
|
where
|
||||||
|
id = $1;
|
||||||
|
|
||||||
|
-- name: CreateUser :one
|
||||||
|
insert into users(username, email, password, claims)
|
||||||
|
values ($1, $2, $3, $4)
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: UpdateUser :one
|
||||||
|
update
|
||||||
|
users
|
||||||
|
set
|
||||||
|
username = $2,
|
||||||
|
email = $3,
|
||||||
|
password = $4,
|
||||||
|
claims = $5
|
||||||
|
where
|
||||||
|
id = $1
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
||||||
|
-- name: DeleteUser :one
|
||||||
|
delete from users
|
||||||
|
where id = $1
|
||||||
|
returning
|
||||||
|
*;
|
||||||
|
|
36
auth/sqlc.yaml
Normal file
36
auth/sqlc.yaml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- engine: "postgresql"
|
||||||
|
queries: "sql/queries"
|
||||||
|
schema: "sql/migrations"
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: "dbc"
|
||||||
|
sql_package: "pgx/v5"
|
||||||
|
out: "dbc"
|
||||||
|
emit_pointers_for_null_types: true
|
||||||
|
emit_json_tags: true
|
||||||
|
json_tags_case_style: camel
|
||||||
|
initialisms: []
|
||||||
|
overrides:
|
||||||
|
- db_type: "timestamptz"
|
||||||
|
go_type:
|
||||||
|
import: "time"
|
||||||
|
type: "Time"
|
||||||
|
- db_type: "timestamptz"
|
||||||
|
nullable: true
|
||||||
|
go_type:
|
||||||
|
import: "time"
|
||||||
|
type: "Time"
|
||||||
|
pointer: true
|
||||||
|
- db_type: "uuid"
|
||||||
|
go_type:
|
||||||
|
import: "github.com/google/uuid"
|
||||||
|
type: "UUID"
|
||||||
|
- column: "users.claims"
|
||||||
|
go_type:
|
||||||
|
import: "github.com/golang-jwt/jwt/v5"
|
||||||
|
package: "jwt"
|
||||||
|
type: "MapClaims"
|
||||||
|
|
||||||
|
|
272
auth/users.go
Normal file
272
auth/users.go
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alexedwards/argon2id"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgerrcode"
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/zoriya/kyoo/keibi/dbc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
// Primary key in database
|
||||||
|
Pk int32 `json:"-"`
|
||||||
|
// Id of the user.
|
||||||
|
Id uuid.UUID `json:"id"`
|
||||||
|
// Username of the user. Can be used as a login.
|
||||||
|
Username string `json:"username"`
|
||||||
|
// Email of the user. Can be used as a login.
|
||||||
|
Email string `json:"email" format:"email"`
|
||||||
|
// When was this account created?
|
||||||
|
CreatedDate time.Time `json:"createdDate"`
|
||||||
|
// When was the last time this account made any authorized request?
|
||||||
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
|
// List of custom claims JWT created via get /jwt will have
|
||||||
|
Claims jwt.MapClaims `json:"claims"`
|
||||||
|
// List of other login method available for this user. Access tokens wont be returned here.
|
||||||
|
Oidc map[string]OidcHandle `json:"oidc,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OidcHandle struct {
|
||||||
|
// Id of this oidc handle.
|
||||||
|
Id string `json:"id"`
|
||||||
|
// Username of the user on the external service.
|
||||||
|
Username string `json:"username"`
|
||||||
|
// Link to the profile of the user on the external service. Null if unknown or irrelevant.
|
||||||
|
ProfileUrl *string `json:"profileUrl" format:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterDto struct {
|
||||||
|
// Username of the new account, can't contain @ signs. Can be used for login.
|
||||||
|
Username string `json:"username" validate:"required,excludes=@"`
|
||||||
|
// Valid email that could be used for forgotten password requests. Can be used for login.
|
||||||
|
Email string `json:"email" validate:"required,email" format:"email"`
|
||||||
|
// Password to use.
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapDbUser(user *dbc.User) User {
|
||||||
|
return User{
|
||||||
|
Pk: user.Pk,
|
||||||
|
Id: user.Id,
|
||||||
|
Username: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
CreatedDate: user.CreatedDate,
|
||||||
|
LastSeen: user.LastSeen,
|
||||||
|
Claims: user.Claims,
|
||||||
|
Oidc: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MapOidc(oidc *dbc.GetUserRow) OidcHandle {
|
||||||
|
return OidcHandle{
|
||||||
|
Id: *oidc.Id,
|
||||||
|
Username: *oidc.Username,
|
||||||
|
ProfileUrl: oidc.ProfileUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary List all users
|
||||||
|
// @Description List all users existing in this instance.
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt[users.read]
|
||||||
|
// @Param afterId query string false "used for pagination." Format(uuid)
|
||||||
|
// @Success 200 {object} User[]
|
||||||
|
// @Failure 400 {object} problem.Problem "Invalid after id"
|
||||||
|
// @Router /users [get]
|
||||||
|
func (h *Handler) ListUsers(c echo.Context) error {
|
||||||
|
err := CheckPermissions(c, []string{"user.read"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
limit := int32(20)
|
||||||
|
id := c.Param("afterId")
|
||||||
|
|
||||||
|
var users []dbc.User
|
||||||
|
if id == "" {
|
||||||
|
users, err = h.db.GetAllUsers(ctx, limit)
|
||||||
|
} else {
|
||||||
|
uid, uerr := uuid.Parse(id)
|
||||||
|
if uerr != nil {
|
||||||
|
return echo.NewHTTPError(400, "Invalid `afterId` parameter, uuid was expected")
|
||||||
|
}
|
||||||
|
users, err = h.db.GetAllUsersAfter(ctx, dbc.GetAllUsersAfterParams{
|
||||||
|
Limit: limit,
|
||||||
|
AfterId: uid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []User
|
||||||
|
for _, user := range users {
|
||||||
|
ret = append(ret, MapDbUser(&user))
|
||||||
|
}
|
||||||
|
// TODO: switch to a Page
|
||||||
|
return c.JSON(200, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get user
|
||||||
|
// @Description Get informations about a user from it's id
|
||||||
|
// @Tags users
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt[users.read]
|
||||||
|
// @Param id path string true "The id of the user" Format(uuid)
|
||||||
|
// @Success 200 {object} User
|
||||||
|
// @Failure 404 {object} problem.Problem "No user with the given id found"
|
||||||
|
// @Router /users/{id} [get]
|
||||||
|
func (h *Handler) GetUser(c echo.Context) error {
|
||||||
|
err := CheckPermissions(c, []string{"user.read"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(400, "Invalid id")
|
||||||
|
}
|
||||||
|
dbuser, err := h.db.GetUser(context.Background(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := MapDbUser(&dbuser[0].User)
|
||||||
|
for _, oidc := range dbuser {
|
||||||
|
if oidc.Provider != nil {
|
||||||
|
user.Oidc[*oidc.Provider] = MapOidc(&oidc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get me
|
||||||
|
// @Description Get informations about the currently connected user
|
||||||
|
// @Tags users
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt
|
||||||
|
// @Success 200 {object} User
|
||||||
|
// @Failure 401 {object} problem.Problem "Missing jwt token"
|
||||||
|
// @Failure 403 {object} problem.Problem "Invalid jwt token (or expired)"
|
||||||
|
// @Router /users/me [get]
|
||||||
|
func (h *Handler) GetMe(c echo.Context) error {
|
||||||
|
id, err := GetCurrentUserId(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dbuser, err := h.db.GetUser(context.Background(), id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := MapDbUser(&dbuser[0].User)
|
||||||
|
for _, oidc := range dbuser {
|
||||||
|
if oidc.Provider != nil {
|
||||||
|
user.Oidc[*oidc.Provider] = MapOidc(&oidc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Register
|
||||||
|
// @Description Register as a new user and open a session for it
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param device query string false "The device the created session will be used on"
|
||||||
|
// @Param user body RegisterDto false "Registration informations"
|
||||||
|
// @Success 201 {object} dbc.Session
|
||||||
|
// @Failure 400 {object} problem.Problem "Invalid register body"
|
||||||
|
// @Success 409 {object} problem.Problem "Duplicated email or username"
|
||||||
|
// @Router /users [post]
|
||||||
|
func (h *Handler) Register(c echo.Context) error {
|
||||||
|
var req RegisterDto
|
||||||
|
err := c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if err = c.Validate(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pass, err := argon2id.CreateHash(req.Password, argon2id.DefaultParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
duser, err := h.db.CreateUser(context.Background(), dbc.CreateUserParams{
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
Password: &pass,
|
||||||
|
Claims: h.config.DefaultClaims,
|
||||||
|
})
|
||||||
|
if ErrIs(err, pgerrcode.UniqueViolation) {
|
||||||
|
return echo.NewHTTPError(409, "Email or username already taken")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user := MapDbUser(&duser)
|
||||||
|
return h.createSession(c, &user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Delete user
|
||||||
|
// @Description Delete an account and all it's sessions.
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt[users.delete]
|
||||||
|
// @Param id path string false "User id of the user to delete" Format(uuid)
|
||||||
|
// @Success 200 {object} User
|
||||||
|
// @Failure 404 {object} problem.Problem "Invalid id format"
|
||||||
|
// @Failure 404 {object} problem.Problem "Invalid user id"
|
||||||
|
// @Router /users/{id} [delete]
|
||||||
|
func (h *Handler) DeleteUser(c echo.Context) error {
|
||||||
|
uid, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := h.db.DeleteUser(context.Background(), uid)
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(404, "No user found with given id")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(200, MapDbUser(&ret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Delete self
|
||||||
|
// @Description Delete your account and all your sessions
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt
|
||||||
|
// @Success 200 {object} User
|
||||||
|
// @Router /users/me [delete]
|
||||||
|
func (h *Handler) DeleteSelf(c echo.Context) error {
|
||||||
|
uid, err := GetCurrentUserId(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := h.db.DeleteUser(context.Background(), uid)
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(403, "Invalid token, user already deleted.")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.JSON(200, MapDbUser(&ret))
|
||||||
|
}
|
73
auth/utils.go
Normal file
73
auth/utils.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetCurrentUserId(c echo.Context) (uuid.UUID, error) {
|
||||||
|
user := c.Get("user").(*jwt.Token)
|
||||||
|
if user == nil {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(401, "Unauthorized")
|
||||||
|
}
|
||||||
|
sub, err := user.Claims.GetSubject()
|
||||||
|
if err != nil {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(403, "Could not retrive subject")
|
||||||
|
}
|
||||||
|
ret, err := uuid.Parse(sub)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(403, "Invalid id")
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPermissions(c echo.Context, perms []string) error {
|
||||||
|
token, ok := c.Get("user").(*jwt.Token)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(401, "Not logged in")
|
||||||
|
}
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(403, "Could not retrieve claims")
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions_claims, ok := claims["permissions"]
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(403, fmt.Sprintf("Missing permissions: %s.", ", "))
|
||||||
|
}
|
||||||
|
permissions, ok := permissions_claims.([]string)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(403, "Invalid permission claim.")
|
||||||
|
}
|
||||||
|
|
||||||
|
missing := make([]string, 0)
|
||||||
|
for _, perm := range perms {
|
||||||
|
if !slices.Contains(permissions, perm) {
|
||||||
|
missing = append(missing, perm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) != 0 {
|
||||||
|
return echo.NewHTTPError(
|
||||||
|
403,
|
||||||
|
fmt.Sprintf("Missing permissions: %s.", strings.Join(missing, ", ")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrIs(err error, code string) bool {
|
||||||
|
var pgerr *pgconn.PgError
|
||||||
|
|
||||||
|
if !errors.As(err, &pgerr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return pgerr.Code == code
|
||||||
|
}
|
@ -80,4 +80,3 @@ Login
|
|||||||
Should Be Equal As Strings ${res["body"]} ${me["body"]}
|
Should Be Equal As Strings ${res["body"]} ${me["body"]}
|
||||||
|
|
||||||
[Teardown] DELETE /auth/me
|
[Teardown] DELETE /auth/me
|
||||||
|
|
@ -60,6 +60,23 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
||||||
|
|
||||||
|
auth:
|
||||||
|
build: ./auth
|
||||||
|
restart: on-failure
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.auth.rule=PathPrefix(`/auth/`)"
|
||||||
|
- "traefik.http.routers.auth.middlewares=auth-sp"
|
||||||
|
- "traefik.http.middlewares.auth-sp.stripprefix.prefixes=/auth"
|
||||||
|
- "traefik.http.middlewares.auth-sp.stripprefix.forceSlash=false"
|
||||||
|
profiles:
|
||||||
|
- "v5"
|
||||||
|
|
||||||
scanner:
|
scanner:
|
||||||
build: ./scanner
|
build: ./scanner
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
|
@ -84,6 +84,27 @@ services:
|
|||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
- "traefik.http.routers.front.rule=PathPrefix(`/`)"
|
||||||
|
|
||||||
|
auth:
|
||||||
|
build:
|
||||||
|
context: ./auth
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
restart: on-failure
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "4568:4568"
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
volumes:
|
||||||
|
- ./auth:/app
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.auth.rule=PathPrefix(`/auth/`)"
|
||||||
|
- "traefik.http.routers.auth.middlewares=auth-sp"
|
||||||
|
- "traefik.http.middlewares.auth-sp.stripprefix.prefixes=/auth"
|
||||||
|
- "traefik.http.middlewares.auth-sp.stripprefix.forceSlash=false"
|
||||||
|
|
||||||
scanner:
|
scanner:
|
||||||
build: ./scanner
|
build: ./scanner
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
|
45
pyproject.toml
Normal file
45
pyproject.toml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
[tool.robotidy]
|
||||||
|
diff = false
|
||||||
|
overwrite = true
|
||||||
|
verbose = false
|
||||||
|
separator = "space"
|
||||||
|
spacecount = 2
|
||||||
|
line_length = 120
|
||||||
|
lineseparator = "native"
|
||||||
|
skip_gitignore = true
|
||||||
|
ignore_git_dir = true
|
||||||
|
configure = [
|
||||||
|
"AddMissingEnd:enabled=True",
|
||||||
|
"NormalizeSeparators:enabled=True",
|
||||||
|
"DiscardEmptySections:enabled=True",
|
||||||
|
"MergeAndOrderSections:enabled=True",
|
||||||
|
"RemoveEmptySettings:enabled=True",
|
||||||
|
"ReplaceEmptyValues:enabled=True",
|
||||||
|
"ReplaceWithVAR:enabled=False",
|
||||||
|
"NormalizeAssignments:enabled=True",
|
||||||
|
"GenerateDocumentation:enabled=False",
|
||||||
|
"OrderSettings:enabled=True",
|
||||||
|
"OrderSettingsSection:enabled=True",
|
||||||
|
"NormalizeTags:enabled=True",
|
||||||
|
"OrderTags:enabled=False",
|
||||||
|
"RenameVariables:enabled=False",
|
||||||
|
"IndentNestedKeywords:enabled=False",
|
||||||
|
"AlignSettingsSection:enabled=True",
|
||||||
|
"AlignVariablesSection:enabled=True",
|
||||||
|
"AlignTemplatedTestCases:enabled=False",
|
||||||
|
"AlignTestCasesSection:enabled=False",
|
||||||
|
"AlignKeywordsSection:enabled=False",
|
||||||
|
"NormalizeNewLines:enabled=True",
|
||||||
|
"NormalizeSectionHeaderName:enabled=True",
|
||||||
|
"NormalizeSettingName:enabled=True",
|
||||||
|
"ReplaceRunKeywordIf:enabled=True",
|
||||||
|
"SplitTooLongLine:enabled=True",
|
||||||
|
"SmartSortKeywords:enabled=False",
|
||||||
|
"RenameTestCases:enabled=False",
|
||||||
|
"RenameKeywords:enabled=False",
|
||||||
|
"ReplaceReturns:enabled=True",
|
||||||
|
"ReplaceBreakContinue:enabled=True",
|
||||||
|
"InlineIf:enabled=True",
|
||||||
|
"Translate:enabled=False",
|
||||||
|
"NormalizeComments:enabled=True",
|
||||||
|
]
|
@ -11,6 +11,9 @@
|
|||||||
dataclasses-json
|
dataclasses-json
|
||||||
msgspec
|
msgspec
|
||||||
langcodes
|
langcodes
|
||||||
|
|
||||||
|
# robotframework
|
||||||
|
# restinstance needs to be packaged
|
||||||
]);
|
]);
|
||||||
dotnet = with pkgs.dotnetCorePackages;
|
dotnet = with pkgs.dotnetCorePackages;
|
||||||
combinePackages [
|
combinePackages [
|
||||||
@ -38,6 +41,9 @@ in
|
|||||||
biome
|
biome
|
||||||
kubernetes-helm
|
kubernetes-helm
|
||||||
go-migrate
|
go-migrate
|
||||||
|
sqlc
|
||||||
|
go-swag
|
||||||
|
robotframework-tidy
|
||||||
];
|
];
|
||||||
|
|
||||||
DOTNET_ROOT = "${dotnet}";
|
DOTNET_ROOT = "${dotnet}";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user