mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 15:30:34 -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.
|
||||
PUBLIC_URL=http://localhost:5000
|
||||
PUBLIC_URL=http://localhost:8901
|
||||
|
||||
# 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
|
||||
|
20
.github/workflows/robot.yml
vendored
20
.github/workflows/robot.yml
vendored
@ -14,11 +14,13 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Pull images
|
||||
run: |
|
||||
cp .env.example .env
|
||||
docker compose version
|
||||
docker compose pull
|
||||
- name: Robot cache
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
cache: 'pip'
|
||||
|
||||
- run: pip install -r requirements.txt
|
||||
|
||||
- name: Docker cache
|
||||
uses: satackey/action-docker-layer-caching@v0.0.11
|
||||
@ -26,18 +28,18 @@ jobs:
|
||||
|
||||
- name: Start the service
|
||||
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
|
||||
run: |
|
||||
docker compose ps -a
|
||||
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
|
||||
run: |
|
||||
pip install -r back/tests/robot/requirements.txt
|
||||
robot -d out back/tests/robot/
|
||||
robot -d out $(find -type d -name robot)
|
||||
|
||||
- name: Show logs
|
||||
if: failure()
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -7,4 +7,5 @@ log.html
|
||||
output.xml
|
||||
report.html
|
||||
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"]}
|
||||
|
||||
[Teardown] DELETE /auth/me
|
||||
|
@ -60,6 +60,23 @@ services:
|
||||
- "traefik.enable=true"
|
||||
- "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:
|
||||
build: ./scanner
|
||||
restart: on-failure
|
||||
|
@ -84,6 +84,27 @@ services:
|
||||
- "traefik.enable=true"
|
||||
- "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:
|
||||
build: ./scanner
|
||||
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
|
||||
msgspec
|
||||
langcodes
|
||||
|
||||
# robotframework
|
||||
# restinstance needs to be packaged
|
||||
]);
|
||||
dotnet = with pkgs.dotnetCorePackages;
|
||||
combinePackages [
|
||||
@ -38,6 +41,9 @@ in
|
||||
biome
|
||||
kubernetes-helm
|
||||
go-migrate
|
||||
sqlc
|
||||
go-swag
|
||||
robotframework-tidy
|
||||
];
|
||||
|
||||
DOTNET_ROOT = "${dotnet}";
|
||||
|
Loading…
x
Reference in New Issue
Block a user