mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 10:37: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