mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Auth cleanups (#872)
This commit is contained in:
commit
a903d88a66
@ -1,4 +1,4 @@
|
||||
name: RobotTests
|
||||
name: HurlTests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -9,7 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Robot tests Auth
|
||||
name: Hurl tests Auth
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
@ -27,13 +27,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Robot cache
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
cache: 'pip'
|
||||
|
||||
- run: pip install -r requirements.txt
|
||||
- uses: gacts/install-hurl@v1
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
@ -44,22 +38,18 @@ jobs:
|
||||
working-directory: ./auth
|
||||
run: |
|
||||
go mod download
|
||||
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
- name: Build
|
||||
working-directory: ./auth
|
||||
run: |
|
||||
sqlc generate
|
||||
swag init --parseDependency
|
||||
go build -o ./keibi
|
||||
|
||||
- name: Run robot tests
|
||||
- name: Run hurl tests
|
||||
working-directory: ./auth
|
||||
run: |
|
||||
./keibi > logs &
|
||||
wget --retry-connrefused --retry-on-http-error=502 http://localhost:4568/health
|
||||
robot -d out robot
|
||||
hurl --error-format long --variable host=http://localhost:4568 tests/*
|
||||
env:
|
||||
POSTGRES_SERVER: localhost
|
||||
|
@ -3,9 +3,12 @@
|
||||
|
||||
KYOO_PREFIX=/api
|
||||
|
||||
|
||||
# either an hard-coded secret to decode jwts or empty to use keibi's public secret.
|
||||
# this should only be used in tests
|
||||
JWT_SECRET=
|
||||
# used to verify who's making the jwt
|
||||
JWT_ISSUER=$PUBLIC_URL
|
||||
# keibi's server to retrieve the public jwt secret
|
||||
AUHT_SERVER=http://auth:4568
|
||||
|
||||
|
22
api/bun.lock
22
api/bun.lock
@ -4,7 +4,7 @@
|
||||
"": {
|
||||
"name": "api",
|
||||
"dependencies": {
|
||||
"@elysiajs/swagger": "^1.2.2",
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"drizzle-orm": "0.39.0",
|
||||
@ -27,7 +27,7 @@
|
||||
"packages": {
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@elysiajs/swagger": ["@elysiajs/swagger@1.2.2", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-DG0PbX/wzQNQ6kIpFFPCvmkkWTIbNWDS7lVLv3Puy6ONklF14B4NnbDfpYjX1hdSYKeCqKBBOuenh6jKm8tbYA=="],
|
||||
"@elysiajs/swagger": ["@elysiajs/swagger@github:zoriya/elysia-swagger#ef89c17", { "dependencies": { "@scalar/themes": "^0.9.81", "@scalar/types": "^0.1.3", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "zoriya-elysia-swagger-ef89c17"],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="],
|
||||
|
||||
@ -121,15 +121,15 @@
|
||||
|
||||
"@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="],
|
||||
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="],
|
||||
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
|
||||
|
||||
"@scalar/themes": ["@scalar/themes@0.9.79", "", { "dependencies": { "@scalar/types": "0.1.1" } }, "sha512-zWiHCZAIjPGa8X9o/NORBPRMTMblLEz2+2RcfW9yIKNO/8H4Gz0rltiGGlJ6vX0o+qHwx7AdgfY+7njmWQR4ng=="],
|
||||
"@scalar/themes": ["@scalar/themes@0.9.81", "", { "dependencies": { "@scalar/types": "0.1.3" } }, "sha512-asTgdqo8ZYibBBWVYy0503qPx3cvwDlYNuc/cLbrCmTav0MAEL4wNb/gz9iScMVSMwhdkSkL5g9LPdr2mQrHzw=="],
|
||||
|
||||
"@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="],
|
||||
"@scalar/types": ["@scalar/types@0.1.3", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-Fxtgjp5wHhTzXiyODYWIoTsTy3oFC70vme+0I7MNwd8i6D8qplFNnpURueqBuP4MglBM2ZhFv3hPLw4D69anDA=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.30", "", {}, "sha512-gFB3BiqjDxEoadW0zn+xyMVb7cLxPCoblVn2C/BKpI41WPYi2d6fwHAlynPNZ5O/Q4WEiujdnJzVtvG/Jc2CBQ=="],
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.31", "", {}, "sha512-qQ71T9DsITbX3dVCrcBERbs11YuSMg3wZPnT472JhqhWGPdiLgyvihJXU8m+ADJtJvRdjATIiACJD22dEknBrQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
|
||||
"@types/node": ["@types/node@22.13.13", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.11.11", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^4.0.1" } }, "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw=="],
|
||||
|
||||
@ -141,7 +141,7 @@
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
|
||||
"bun-types": ["bun-types@1.2.6", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-FbCKyr5KDiPULUzN/nm5oqQs9nXCHD8dVc64BArxJadCvbNzAI6lUWGh9fSJZWeDIRD38ikceBU8Kj/Uh+53oQ=="],
|
||||
|
||||
"char-info": ["char-info@0.3.5", "", { "dependencies": { "node-interval-tree": "^1.3.3" } }, "sha512-gRslEBFEcuLMGLNO1EFIrdN1MMUfO+aqa7y8iWzNyAzB3mYKnTIvP+ioW3jpyeEvqA5WapVLIPINGtFjEIH4cQ=="],
|
||||
|
||||
@ -199,7 +199,7 @@
|
||||
|
||||
"pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"pg": ["pg@8.14.0", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-nXbVpyoaXVmdqlKEzToFf37qzyeeh7mbiXsnoWvstSqohj88yaa/I/Rq/HEVn2QPSZEuLIJa/jSpRDyzjEx4FQ=="],
|
||||
"pg": ["pg@8.14.1", "", { "dependencies": { "pg-connection-string": "^2.7.0", "pg-pool": "^3.8.0", "pg-protocol": "^1.8.0", "pg-types": "^2.1.0", "pgpass": "1.x" }, "optionalDependencies": { "pg-cloudflare": "^1.1.1" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.1.1", "", {}, "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q=="],
|
||||
|
||||
@ -259,8 +259,6 @@
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@scalar/themes/@scalar/types": ["@scalar/types@0.1.1", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-LlUX6AmOOGoRqOMoO835V2FezM1KiO5UlvQC3poT/s7oqD6ranqwRNFxyrPz/IxClPYR+SV1yBUSNKely4ZQhQ=="],
|
||||
|
||||
"pg/pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
@ -307,8 +305,6 @@
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
|
||||
"@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
|
||||
|
||||
"pg/pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"pg/pg-types/postgres-bytea": ["postgres-bytea@1.0.0", "", {}, "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w=="],
|
||||
|
@ -9,7 +9,7 @@
|
||||
"format": "biome check --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@elysiajs/swagger": "^1.2.2",
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"blurhash": "^2.0.5",
|
||||
"drizzle-kit": "^0.30.4",
|
||||
"drizzle-orm": "0.39.0",
|
||||
|
@ -34,10 +34,13 @@ export const auth = new Elysia({ name: "auth" })
|
||||
permissions(perms: string[]) {
|
||||
return {
|
||||
resolve: async ({ headers: { authorization }, error }) => {
|
||||
console.log(process.env.JWT_ISSUER);
|
||||
const bearer = authorization?.slice(7);
|
||||
if (!bearer) return { jwt: false };
|
||||
// @ts-expect-error ts can't understand that there's two overload idk why
|
||||
const { payload } = await jwtVerify(bearer, jwtSecret ?? jwks);
|
||||
const { payload } = await jwtVerify(bearer, jwtSecret ?? jwks, {
|
||||
issuer: process.env.JWT_ISSUER,
|
||||
});
|
||||
// TODO: use perms
|
||||
return { jwt: validator.Decode<typeof Jwt>(payload) };
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { swagger } from "@elysiajs/swagger";
|
||||
import Elysia from "elysia";
|
||||
import { app } from "./base";
|
||||
import { processImages } from "./controllers/seed/images";
|
||||
import { migrate } from "./db";
|
||||
@ -9,9 +10,15 @@ await migrate();
|
||||
// run image processor task in background
|
||||
processImages();
|
||||
|
||||
app
|
||||
new Elysia()
|
||||
.use(
|
||||
swagger({
|
||||
scalarConfig: {
|
||||
sources: [
|
||||
{ slug: "kyoo", url: "/swagger/json" },
|
||||
{ slug: "keibi", url: "http://localhost:4568/auth/swagger/doc.json" },
|
||||
],
|
||||
},
|
||||
documentation: {
|
||||
info: {
|
||||
title: "Kyoo",
|
||||
@ -72,6 +79,7 @@ app
|
||||
},
|
||||
}),
|
||||
)
|
||||
.use(app)
|
||||
.listen(3567);
|
||||
|
||||
console.log(`Api running at ${app.server?.hostname}:${app.server?.port}`);
|
||||
|
@ -8,7 +8,7 @@ export const desc = {
|
||||
`,
|
||||
|
||||
after: comment`
|
||||
Id of the cursor in the pagination.
|
||||
Cursor for the pagination.
|
||||
You can ignore this and only use the prev/next field in the response.
|
||||
`,
|
||||
|
||||
|
@ -4,6 +4,16 @@
|
||||
# http route prefix (will listen to $KEIBI_PREFIX/users for example)
|
||||
KEIBI_PREFIX=""
|
||||
|
||||
# path of the private key used to sign jwts. If this is empty, a new one will be generated on startup
|
||||
RSA_PRIVATE_KEY_PATH=""
|
||||
|
||||
# json object with the claims to add to every jwt (this is read when creating a new user)
|
||||
EXTRA_CLAIMS='{}'
|
||||
# json object with the claims to add to every jwt of the FIRST user (this can be used to mark the first user as admin).
|
||||
# Those claims are merged with the `EXTRA_CLAIMS`.
|
||||
FIRST_USER_CLAIMS='{}'
|
||||
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
|
||||
GUEST_CLAIMS=""
|
||||
# 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:8901
|
||||
|
||||
|
@ -12,7 +12,7 @@ COPY sql ./sql
|
||||
RUN sqlc generate
|
||||
|
||||
COPY . .
|
||||
RUN swag init --parseDependency
|
||||
RUN swag init --parseDependency --outputTypes json,go
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /keibi
|
||||
|
||||
FROM gcr.io/distroless/base-debian11
|
||||
|
@ -10,6 +10,7 @@
|
||||
- Username/password login
|
||||
- OIDC (login via Google, Discord, Authentik, whatever)
|
||||
- Custom jwt claims (for your role/permissions handling or something else)
|
||||
- Guest handling (only if using `GUEST_CLAIMS`)
|
||||
- Api keys support
|
||||
- Optionally [Federated](#federated)
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"maps"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@ -19,64 +20,77 @@ type Configuration struct {
|
||||
JwtPublicKey *rsa.PublicKey
|
||||
PublicUrl string
|
||||
DefaultClaims jwt.MapClaims
|
||||
FirstUserClaims jwt.MapClaims
|
||||
GuestClaims jwt.MapClaims
|
||||
ExpirationDelay time.Duration
|
||||
}
|
||||
|
||||
var DefaultConfig = Configuration{
|
||||
DefaultClaims: make(jwt.MapClaims),
|
||||
FirstUserClaims: 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
|
||||
}
|
||||
}
|
||||
|
||||
ret.PublicUrl = os.Getenv("PUBLIC_URL")
|
||||
ret.Prefix = os.Getenv("KEIBI_PREFIX")
|
||||
|
||||
if ret.JwtPrivateKey == nil {
|
||||
claims := os.Getenv("EXTRA_CLAIMS")
|
||||
if claims != "" {
|
||||
err := json.Unmarshal([]byte(claims), &ret.DefaultClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
claims = os.Getenv("FIRST_USER_CLAIMS")
|
||||
if claims != "" {
|
||||
err := json.Unmarshal([]byte(claims), &ret.FirstUserClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maps.Insert(ret.FirstUserClaims, maps.All(ret.DefaultClaims))
|
||||
} else {
|
||||
ret.FirstUserClaims = ret.DefaultClaims
|
||||
}
|
||||
|
||||
claims = os.Getenv("GUEST_CLAIMS")
|
||||
if claims != "" {
|
||||
err := json.Unmarshal([]byte(claims), &ret.GuestClaims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH")
|
||||
if rsa_pk_path != "" {
|
||||
privateKeyData, err := os.ReadFile(rsa_pk_path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(privateKeyData)
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret.JwtPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
pkcs8Key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.JwtPrivateKey = pkcs8Key.(*rsa.PrivateKey)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
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
|
||||
|
@ -1,73 +0,0 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: config.sql
|
||||
|
||||
package dbc
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const deleteConfig = `-- name: DeleteConfig :one
|
||||
delete from config
|
||||
where key = $1
|
||||
returning
|
||||
key, value
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteConfig(ctx context.Context, key string) (Config, error) {
|
||||
row := q.db.QueryRow(ctx, deleteConfig, key)
|
||||
var i Config
|
||||
err := row.Scan(&i.Key, &i.Value)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const loadConfig = `-- name: LoadConfig :many
|
||||
select
|
||||
key, value
|
||||
from
|
||||
config
|
||||
`
|
||||
|
||||
func (q *Queries) LoadConfig(ctx context.Context) ([]Config, error) {
|
||||
rows, err := q.db.Query(ctx, loadConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Config
|
||||
for rows.Next() {
|
||||
var i Config
|
||||
if err := rows.Scan(&i.Key, &i.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const saveConfig = `-- name: SaveConfig :one
|
||||
insert into config(key, value)
|
||||
values ($1, $2)
|
||||
on conflict (key)
|
||||
do update set
|
||||
value = excluded.value
|
||||
returning
|
||||
key, value
|
||||
`
|
||||
|
||||
type SaveConfigParams struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (q *Queries) SaveConfig(ctx context.Context, arg SaveConfigParams) (Config, error) {
|
||||
row := q.db.QueryRow(ctx, saveConfig, arg.Key, arg.Value)
|
||||
var i Config
|
||||
err := row.Scan(&i.Key, &i.Value)
|
||||
return i, err
|
||||
}
|
@ -11,11 +11,6 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type OidcHandle struct {
|
||||
UserPk int32 `json:"userPk"`
|
||||
Provider string `json:"provider"`
|
||||
|
@ -14,16 +14,25 @@ import (
|
||||
|
||||
const createUser = `-- name: CreateUser :one
|
||||
insert into users(username, email, password, claims)
|
||||
values ($1, $2, $3, $4)
|
||||
values ($1, $2, $3, case when not exists (
|
||||
select
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
from
|
||||
users) then
|
||||
$4::jsonb
|
||||
else
|
||||
$5::jsonb
|
||||
end)
|
||||
returning
|
||||
pk, id, username, email, password, claims, created_date, last_seen
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
Claims jwt.MapClaims `json:"claims"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Password *string `json:"password"`
|
||||
FirstClaims interface{} `json:"firstClaims"`
|
||||
Claims interface{} `json:"claims"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
|
||||
@ -31,6 +40,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
||||
arg.Username,
|
||||
arg.Email,
|
||||
arg.Password,
|
||||
arg.FirstClaims,
|
||||
arg.Claims,
|
||||
)
|
||||
var i User
|
||||
|
@ -22,21 +22,21 @@ const docTemplate = `{
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/info": {
|
||||
"/.well-known/jwks.json": {
|
||||
"get": {
|
||||
"description": "Get info like the public key used to sign the jwts.",
|
||||
"description": "Get the jwks info, used to validate jwts.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jwt"
|
||||
],
|
||||
"summary": "Info",
|
||||
"summary": "Jwks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.Info"
|
||||
"$ref": "#/definitions/main.JwkSet"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,15 +62,19 @@ const docTemplate = `{
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.Jwt"
|
||||
},
|
||||
"headers": {
|
||||
"Authorization": {
|
||||
"type": "string",
|
||||
"description": "Jwt (same value as the returned token)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing session token",
|
||||
"schema": {}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid session token (or expired)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,6 +95,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "android tv",
|
||||
"description": "The device the created session will be used on",
|
||||
"name": "device",
|
||||
"in": "query"
|
||||
@ -108,24 +113,26 @@ const docTemplate = `{
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dbc.Session"
|
||||
"$ref": "#/definitions/main.SessionWToken"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid login body",
|
||||
"schema": {}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid password",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Account does not exists",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "User does not have a password (registered via oidc, please login via oidc)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -152,21 +159,17 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/main.Session"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid session id",
|
||||
"schema": {}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing jwt token",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid jwt token (or expired)",
|
||||
"schema": {}
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found with specified id (if not using the /current route)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,11 +188,12 @@ const docTemplate = `{
|
||||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"summary": "Logout",
|
||||
"summary": "Delete other session",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397",
|
||||
"description": "The id of the session to delete",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
@ -203,21 +207,17 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/main.Session"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid session id",
|
||||
"schema": {}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing jwt token",
|
||||
"schema": {}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid jwt token (or expired)",
|
||||
"schema": {}
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found with specified id (if not using the /current route)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid session id",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -245,9 +245,8 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "used for pagination.",
|
||||
"name": "afterId",
|
||||
"name": "after",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
@ -255,12 +254,14 @@ const docTemplate = `{
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.User"
|
||||
"$ref": "#/definitions/main.Page-main_User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"422": {
|
||||
"description": "Invalid after id",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -279,6 +280,7 @@ const docTemplate = `{
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "android",
|
||||
"description": "The device the created session will be used on",
|
||||
"name": "device",
|
||||
"in": "query"
|
||||
@ -296,16 +298,20 @@ const docTemplate = `{
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dbc.Session"
|
||||
"$ref": "#/definitions/main.SessionWToken"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid register body",
|
||||
"schema": {}
|
||||
},
|
||||
"409": {
|
||||
"description": "Duplicated email or username",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid register body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -334,11 +340,15 @@ const docTemplate = `{
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing jwt token",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid jwt token (or expired)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -405,7 +415,15 @@ const docTemplate = `{
|
||||
},
|
||||
"404": {
|
||||
"description": "No user with the given id found",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid id (not a uuid)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -446,45 +464,50 @@ const docTemplate = `{
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid user id",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"dbc.Session": {
|
||||
"main.JwkSet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"device": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastUsed": {
|
||||
"type": "string"
|
||||
},
|
||||
"pk": {
|
||||
"type": "integer"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"userPk": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"publicKey": {
|
||||
"description": "The public key used to sign jwt tokens. It can be used by your services to check if the jwt is valid.",
|
||||
"type": "string"
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"e": {
|
||||
"type": "string",
|
||||
"example": "AQAB"
|
||||
},
|
||||
"key_ops": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"[verify]"
|
||||
]
|
||||
},
|
||||
"kty": {
|
||||
"type": "string",
|
||||
"example": "RSA"
|
||||
},
|
||||
"n": {
|
||||
"type": "string",
|
||||
"example": "oBcXcJUR-Sb8_b4qIj28LRAPxdF_6odRr52K5-ymiEkR2DOlEuXBtM-biWxPESW-U-zhfHzdVLf6ioy5xL0bJTh8BMIorkrDliN3vb81jCvyOMgZ7ATMJpMAQMmSDN7sL3U45r22FaoQufCJMQHmUsZPecdQSgj2aFBiRXxsLleYlSezdBVT_gKH-coqeYXSC_hk-ezSq4aDZ10BlDnZ-FA7-ES3T7nBmJEAU7KDAGeSvbYAfYimOW0r-Vc0xQNuwGCfzZtSexKXDbYbNwOVo3SjfCabq-gMfap_owcHbKicGBZu1LDlh7CpkmLQf_kv6GihM2LWFFh6Vwg2cltiwF22EIPlUDtYTkUR0qRkdNJaNkwV5Vv_6r3pzSmu5ovRriKtlrvJMjlTnLb4_ltsge3fw5Z34cJrsp094FbUc2O6Or4FGEXUldieJCnVRhs2_h6SDcmeMXs1zfvE5GlDnq8tZV6WMJ5Sb4jNO7rs_hTkr23_E6mVg-DdtozGfqzRzhIjPym6D_jVfR6dZv5W0sKwOHRmT7nYq-C7b2sAwmNNII296M4Rq-jn0b5pgSeMDYbIpbIA4thU8LYU0lBZp_ZVwWKG1RFZDxz3k9O5UVth2kTpTWlwn0hB1aAvgXHo6in1CScITGA72p73RbDieNnLFaCK4xUVstkWAKLqPxs"
|
||||
},
|
||||
"use": {
|
||||
"type": "string",
|
||||
"example": "sig"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -493,7 +516,22 @@ const docTemplate = `{
|
||||
"properties": {
|
||||
"token": {
|
||||
"description": "The jwt token you can use for all authorized call to either keibi or other services.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.KError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "No user found with this id"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"example": 404
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -506,11 +544,13 @@ const docTemplate = `{
|
||||
"properties": {
|
||||
"login": {
|
||||
"description": "Either the email or the username.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
},
|
||||
"password": {
|
||||
"description": "Password of the account.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "password1234"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -519,16 +559,38 @@ const docTemplate = `{
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Id of this oidc handle.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"profileUrl": {
|
||||
"description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.",
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
"format": "url",
|
||||
"example": "https://myanimelist.net/profile/zoriya"
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the user on the external service.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.Page-main_User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/main.User"
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"type": "string",
|
||||
"example": "https://kyoo.zoriya.dev/auth/users?after=aoeusth"
|
||||
},
|
||||
"this": {
|
||||
"type": "string",
|
||||
"example": "https://kyoo.zoriya.dev/auth/users"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -543,15 +605,18 @@ const docTemplate = `{
|
||||
"email": {
|
||||
"description": "Valid email that could be used for forgotten password requests. Can be used for login.",
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"format": "email",
|
||||
"example": "kyoo@zoriya.dev"
|
||||
},
|
||||
"password": {
|
||||
"description": "Password to use.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "password1234"
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the new account, can't contain @ signs. Can be used for login.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -560,19 +625,52 @@ const docTemplate = `{
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"description": "When was the session first opened",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"device": {
|
||||
"description": "Device that created the session.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Web - Firefox"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique id of this session. Can be used for calls to DELETE",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"lastUsed": {
|
||||
"description": "Last date this session was used to access a service.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.SessionWToken": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"description": "When was the session first opened",
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"device": {
|
||||
"description": "Device that created the session.",
|
||||
"type": "string",
|
||||
"example": "Web - Firefox"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique id of this session. Can be used for calls to DELETE",
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"lastUsed": {
|
||||
"description": "Last date this session was used to access a service.",
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"example": "lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -584,24 +682,31 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": {
|
||||
"isAdmin": " true"
|
||||
}
|
||||
},
|
||||
"createdDate": {
|
||||
"description": "When was this account created?",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"email": {
|
||||
"description": "Email of the user. Can be used as a login.",
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"format": "email",
|
||||
"example": "kyoo@zoriya.dev"
|
||||
},
|
||||
"id": {
|
||||
"description": "Id of the user.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"lastSeen": {
|
||||
"description": "When was the last time this account made any authorized request?",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"oidc": {
|
||||
"description": "List of other login method available for this user. Access tokens wont be returned here.",
|
||||
@ -612,7 +717,8 @@ const docTemplate = `{
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the user. Can be used as a login.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,21 +16,21 @@
|
||||
"host": "kyoo.zoriya.dev",
|
||||
"basePath": "/auth",
|
||||
"paths": {
|
||||
"/info": {
|
||||
"/.well-known/jwks.json": {
|
||||
"get": {
|
||||
"description": "Get info like the public key used to sign the jwts.",
|
||||
"description": "Get the jwks info, used to validate jwts.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"jwt"
|
||||
],
|
||||
"summary": "Info",
|
||||
"summary": "Jwks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.Info"
|
||||
"$ref": "#/definitions/main.JwkSet"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,15 +56,19 @@
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.Jwt"
|
||||
},
|
||||
"headers": {
|
||||
"Authorization": {
|
||||
"type": "string",
|
||||
"description": "Jwt (same value as the returned token)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing session token",
|
||||
"schema": {}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid session token (or expired)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -85,6 +89,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "android tv",
|
||||
"description": "The device the created session will be used on",
|
||||
"name": "device",
|
||||
"in": "query"
|
||||
@ -102,24 +107,26 @@
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dbc.Session"
|
||||
"$ref": "#/definitions/main.SessionWToken"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid login body",
|
||||
"schema": {}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid password",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Account does not exists",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "User does not have a password (registered via oidc, please login via oidc)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,21 +153,17 @@
|
||||
"$ref": "#/definitions/main.Session"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid session id",
|
||||
"schema": {}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing jwt token",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid jwt token (or expired)",
|
||||
"schema": {}
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found with specified id (if not using the /current route)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,11 +182,12 @@
|
||||
"tags": [
|
||||
"sessions"
|
||||
],
|
||||
"summary": "Logout",
|
||||
"summary": "Delete other session",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397",
|
||||
"description": "The id of the session to delete",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
@ -197,21 +201,17 @@
|
||||
"$ref": "#/definitions/main.Session"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid session id",
|
||||
"schema": {}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing jwt token",
|
||||
"schema": {}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid jwt token (or expired)",
|
||||
"schema": {}
|
||||
},
|
||||
"404": {
|
||||
"description": "Session not found with specified id (if not using the /current route)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid session id",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -239,9 +239,8 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "used for pagination.",
|
||||
"name": "afterId",
|
||||
"name": "after",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
@ -249,12 +248,14 @@
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.User"
|
||||
"$ref": "#/definitions/main.Page-main_User"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"422": {
|
||||
"description": "Invalid after id",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -273,6 +274,7 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"example": "android",
|
||||
"description": "The device the created session will be used on",
|
||||
"name": "device",
|
||||
"in": "query"
|
||||
@ -290,16 +292,20 @@
|
||||
"201": {
|
||||
"description": "Created",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/dbc.Session"
|
||||
"$ref": "#/definitions/main.SessionWToken"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid register body",
|
||||
"schema": {}
|
||||
},
|
||||
"409": {
|
||||
"description": "Duplicated email or username",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid register body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -328,11 +334,15 @@
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing jwt token",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Invalid jwt token (or expired)",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -399,7 +409,15 @@
|
||||
},
|
||||
"404": {
|
||||
"description": "No user with the given id found",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Invalid id (not a uuid)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -440,45 +458,50 @@
|
||||
},
|
||||
"404": {
|
||||
"description": "Invalid user id",
|
||||
"schema": {}
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.KError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"dbc.Session": {
|
||||
"main.JwkSet": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"type": "string"
|
||||
},
|
||||
"device": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastUsed": {
|
||||
"type": "string"
|
||||
},
|
||||
"pk": {
|
||||
"type": "integer"
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
},
|
||||
"userPk": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"publicKey": {
|
||||
"description": "The public key used to sign jwt tokens. It can be used by your services to check if the jwt is valid.",
|
||||
"type": "string"
|
||||
"keys": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"e": {
|
||||
"type": "string",
|
||||
"example": "AQAB"
|
||||
},
|
||||
"key_ops": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": [
|
||||
"[verify]"
|
||||
]
|
||||
},
|
||||
"kty": {
|
||||
"type": "string",
|
||||
"example": "RSA"
|
||||
},
|
||||
"n": {
|
||||
"type": "string",
|
||||
"example": "oBcXcJUR-Sb8_b4qIj28LRAPxdF_6odRr52K5-ymiEkR2DOlEuXBtM-biWxPESW-U-zhfHzdVLf6ioy5xL0bJTh8BMIorkrDliN3vb81jCvyOMgZ7ATMJpMAQMmSDN7sL3U45r22FaoQufCJMQHmUsZPecdQSgj2aFBiRXxsLleYlSezdBVT_gKH-coqeYXSC_hk-ezSq4aDZ10BlDnZ-FA7-ES3T7nBmJEAU7KDAGeSvbYAfYimOW0r-Vc0xQNuwGCfzZtSexKXDbYbNwOVo3SjfCabq-gMfap_owcHbKicGBZu1LDlh7CpkmLQf_kv6GihM2LWFFh6Vwg2cltiwF22EIPlUDtYTkUR0qRkdNJaNkwV5Vv_6r3pzSmu5ovRriKtlrvJMjlTnLb4_ltsge3fw5Z34cJrsp094FbUc2O6Or4FGEXUldieJCnVRhs2_h6SDcmeMXs1zfvE5GlDnq8tZV6WMJ5Sb4jNO7rs_hTkr23_E6mVg-DdtozGfqzRzhIjPym6D_jVfR6dZv5W0sKwOHRmT7nYq-C7b2sAwmNNII296M4Rq-jn0b5pgSeMDYbIpbIA4thU8LYU0lBZp_ZVwWKG1RFZDxz3k9O5UVth2kTpTWlwn0hB1aAvgXHo6in1CScITGA72p73RbDieNnLFaCK4xUVstkWAKLqPxs"
|
||||
},
|
||||
"use": {
|
||||
"type": "string",
|
||||
"example": "sig"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -487,7 +510,22 @@
|
||||
"properties": {
|
||||
"token": {
|
||||
"description": "The jwt token you can use for all authorized call to either keibi or other services.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.KError": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"details": {},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "No user found with this id"
|
||||
},
|
||||
"status": {
|
||||
"type": "integer",
|
||||
"example": 404
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -500,11 +538,13 @@
|
||||
"properties": {
|
||||
"login": {
|
||||
"description": "Either the email or the username.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
},
|
||||
"password": {
|
||||
"description": "Password of the account.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "password1234"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -513,16 +553,38 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"description": "Id of this oidc handle.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"profileUrl": {
|
||||
"description": "Link to the profile of the user on the external service. Null if unknown or irrelevant.",
|
||||
"type": "string",
|
||||
"format": "url"
|
||||
"format": "url",
|
||||
"example": "https://myanimelist.net/profile/zoriya"
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the user on the external service.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.Page-main_User": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/main.User"
|
||||
}
|
||||
},
|
||||
"next": {
|
||||
"type": "string",
|
||||
"example": "https://kyoo.zoriya.dev/auth/users?after=aoeusth"
|
||||
},
|
||||
"this": {
|
||||
"type": "string",
|
||||
"example": "https://kyoo.zoriya.dev/auth/users"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -537,15 +599,18 @@
|
||||
"email": {
|
||||
"description": "Valid email that could be used for forgotten password requests. Can be used for login.",
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"format": "email",
|
||||
"example": "kyoo@zoriya.dev"
|
||||
},
|
||||
"password": {
|
||||
"description": "Password to use.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "password1234"
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the new account, can't contain @ signs. Can be used for login.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -554,19 +619,52 @@
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"description": "When was the session first opened",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"device": {
|
||||
"description": "Device that created the session.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "Web - Firefox"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique id of this session. Can be used for calls to DELETE",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"lastUsed": {
|
||||
"description": "Last date this session was used to access a service.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main.SessionWToken": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"createdDate": {
|
||||
"description": "When was the session first opened",
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"device": {
|
||||
"description": "Device that created the session.",
|
||||
"type": "string",
|
||||
"example": "Web - Firefox"
|
||||
},
|
||||
"id": {
|
||||
"description": "Unique id of this session. Can be used for calls to DELETE",
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"lastUsed": {
|
||||
"description": "Last date this session was used to access a service.",
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"token": {
|
||||
"type": "string",
|
||||
"example": "lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -578,24 +676,31 @@
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": {
|
||||
"isAdmin": " true"
|
||||
}
|
||||
},
|
||||
"createdDate": {
|
||||
"description": "When was this account created?",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"email": {
|
||||
"description": "Email of the user. Can be used as a login.",
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
"format": "email",
|
||||
"example": "kyoo@zoriya.dev"
|
||||
},
|
||||
"id": {
|
||||
"description": "Id of the user.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "e05089d6-9179-4b5b-a63e-94dd5fc2a397"
|
||||
},
|
||||
"lastSeen": {
|
||||
"description": "When was the last time this account made any authorized request?",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "2025-03-29T18:20:05.267Z"
|
||||
},
|
||||
"oidc": {
|
||||
"description": "List of other login method available for this user. Access tokens wont be returned here.",
|
||||
@ -606,7 +711,8 @@
|
||||
},
|
||||
"username": {
|
||||
"description": "Username of the user. Can be used as a login.",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"example": "zoriya"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,426 +0,0 @@
|
||||
basePath: /auth
|
||||
definitions:
|
||||
dbc.Session:
|
||||
properties:
|
||||
createdDate:
|
||||
type: string
|
||||
device:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
lastUsed:
|
||||
type: string
|
||||
pk:
|
||||
type: integer
|
||||
token:
|
||||
type: string
|
||||
userPk:
|
||||
type: integer
|
||||
type: object
|
||||
main.Info:
|
||||
properties:
|
||||
publicKey:
|
||||
description: The public key used to sign jwt tokens. It can be used by your
|
||||
services to check if the jwt is valid.
|
||||
type: string
|
||||
type: object
|
||||
main.Jwt:
|
||||
properties:
|
||||
token:
|
||||
description: The jwt token you can use for all authorized call to either keibi
|
||||
or other services.
|
||||
type: string
|
||||
type: object
|
||||
main.LoginDto:
|
||||
properties:
|
||||
login:
|
||||
description: Either the email or the username.
|
||||
type: string
|
||||
password:
|
||||
description: Password of the account.
|
||||
type: string
|
||||
required:
|
||||
- login
|
||||
- password
|
||||
type: object
|
||||
main.OidcHandle:
|
||||
properties:
|
||||
id:
|
||||
description: Id of this oidc handle.
|
||||
type: string
|
||||
profileUrl:
|
||||
description: Link to the profile of the user on the external service. Null
|
||||
if unknown or irrelevant.
|
||||
format: url
|
||||
type: string
|
||||
username:
|
||||
description: Username of the user on the external service.
|
||||
type: string
|
||||
type: object
|
||||
main.RegisterDto:
|
||||
properties:
|
||||
email:
|
||||
description: Valid email that could be used for forgotten password requests.
|
||||
Can be used for login.
|
||||
format: email
|
||||
type: string
|
||||
password:
|
||||
description: Password to use.
|
||||
type: string
|
||||
username:
|
||||
description: Username of the new account, can't contain @ signs. Can be used
|
||||
for login.
|
||||
type: string
|
||||
required:
|
||||
- email
|
||||
- password
|
||||
- username
|
||||
type: object
|
||||
main.Session:
|
||||
properties:
|
||||
createdDate:
|
||||
description: When was the session first opened
|
||||
type: string
|
||||
device:
|
||||
description: Device that created the session.
|
||||
type: string
|
||||
id:
|
||||
description: Unique id of this session. Can be used for calls to DELETE
|
||||
type: string
|
||||
lastUsed:
|
||||
description: Last date this session was used to access a service.
|
||||
type: string
|
||||
type: object
|
||||
main.User:
|
||||
properties:
|
||||
claims:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: List of custom claims JWT created via get /jwt will have
|
||||
type: object
|
||||
createdDate:
|
||||
description: When was this account created?
|
||||
type: string
|
||||
email:
|
||||
description: Email of the user. Can be used as a login.
|
||||
format: email
|
||||
type: string
|
||||
id:
|
||||
description: Id of the user.
|
||||
type: string
|
||||
lastSeen:
|
||||
description: When was the last time this account made any authorized request?
|
||||
type: string
|
||||
oidc:
|
||||
additionalProperties:
|
||||
$ref: '#/definitions/main.OidcHandle'
|
||||
description: List of other login method available for this user. Access tokens
|
||||
wont be returned here.
|
||||
type: object
|
||||
username:
|
||||
description: Username of the user. Can be used as a login.
|
||||
type: string
|
||||
type: object
|
||||
host: kyoo.zoriya.dev
|
||||
info:
|
||||
contact:
|
||||
name: Repository
|
||||
url: https://github.com/zoriya/kyoo
|
||||
description: Auth system made for kyoo.
|
||||
license:
|
||||
name: GPL-3.0
|
||||
url: https://www.gnu.org/licenses/gpl-3.0.en.html
|
||||
title: Keibi - Kyoo's auth
|
||||
version: "1.0"
|
||||
paths:
|
||||
/info:
|
||||
get:
|
||||
description: Get info like the public key used to sign the jwts.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.Info'
|
||||
summary: Info
|
||||
tags:
|
||||
- jwt
|
||||
/jwt:
|
||||
get:
|
||||
description: Convert a session token to a short lived JWT.
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.Jwt'
|
||||
"401":
|
||||
description: Missing session token
|
||||
schema: {}
|
||||
"403":
|
||||
description: Invalid session token (or expired)
|
||||
schema: {}
|
||||
security:
|
||||
- Token: []
|
||||
summary: Get JWT
|
||||
tags:
|
||||
- jwt
|
||||
/sessions:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Login to your account and open a session
|
||||
parameters:
|
||||
- description: The device the created session will be used on
|
||||
in: query
|
||||
name: device
|
||||
type: string
|
||||
- description: Account informations
|
||||
in: body
|
||||
name: login
|
||||
schema:
|
||||
$ref: '#/definitions/main.LoginDto'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/dbc.Session'
|
||||
"400":
|
||||
description: Invalid login body
|
||||
schema: {}
|
||||
"403":
|
||||
description: Invalid password
|
||||
schema: {}
|
||||
"404":
|
||||
description: Account does not exists
|
||||
schema: {}
|
||||
"422":
|
||||
description: User does not have a password (registered via oidc, please
|
||||
login via oidc)
|
||||
schema: {}
|
||||
summary: Login
|
||||
tags:
|
||||
- sessions
|
||||
/sessions/{id}:
|
||||
delete:
|
||||
description: Delete a session and logout
|
||||
parameters:
|
||||
- description: The id of the session to delete
|
||||
format: uuid
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.Session'
|
||||
"400":
|
||||
description: Invalid session id
|
||||
schema: {}
|
||||
"401":
|
||||
description: Missing jwt token
|
||||
schema: {}
|
||||
"403":
|
||||
description: Invalid jwt token (or expired)
|
||||
schema: {}
|
||||
"404":
|
||||
description: Session not found with specified id (if not using the /current
|
||||
route)
|
||||
schema: {}
|
||||
security:
|
||||
- Jwt: []
|
||||
summary: Logout
|
||||
tags:
|
||||
- sessions
|
||||
/sessions/current:
|
||||
delete:
|
||||
description: Delete a session and logout
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.Session'
|
||||
"400":
|
||||
description: Invalid session id
|
||||
schema: {}
|
||||
"401":
|
||||
description: Missing jwt token
|
||||
schema: {}
|
||||
"403":
|
||||
description: Invalid jwt token (or expired)
|
||||
schema: {}
|
||||
"404":
|
||||
description: Session not found with specified id (if not using the /current
|
||||
route)
|
||||
schema: {}
|
||||
security:
|
||||
- Jwt: []
|
||||
summary: Logout
|
||||
tags:
|
||||
- sessions
|
||||
/users:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: List all users existing in this instance.
|
||||
parameters:
|
||||
- description: used for pagination.
|
||||
format: uuid
|
||||
in: query
|
||||
name: afterId
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.User'
|
||||
"400":
|
||||
description: Invalid after id
|
||||
schema: {}
|
||||
security:
|
||||
- Jwt:
|
||||
- users.read
|
||||
summary: List all users
|
||||
tags:
|
||||
- users
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Register as a new user and open a session for it
|
||||
parameters:
|
||||
- description: The device the created session will be used on
|
||||
in: query
|
||||
name: device
|
||||
type: string
|
||||
- description: Registration informations
|
||||
in: body
|
||||
name: user
|
||||
schema:
|
||||
$ref: '#/definitions/main.RegisterDto'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/dbc.Session'
|
||||
"400":
|
||||
description: Invalid register body
|
||||
schema: {}
|
||||
"409":
|
||||
description: Duplicated email or username
|
||||
schema: {}
|
||||
summary: Register
|
||||
tags:
|
||||
- users
|
||||
/users/{id}:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete an account and all it's sessions.
|
||||
parameters:
|
||||
- description: User id of the user to delete
|
||||
format: uuid
|
||||
in: path
|
||||
name: id
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.User'
|
||||
"404":
|
||||
description: Invalid user id
|
||||
schema: {}
|
||||
security:
|
||||
- Jwt:
|
||||
- users.delete
|
||||
summary: Delete user
|
||||
tags:
|
||||
- users
|
||||
get:
|
||||
description: Get informations about a user from it's id
|
||||
parameters:
|
||||
- description: The id of the user
|
||||
format: uuid
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.User'
|
||||
"404":
|
||||
description: No user with the given id found
|
||||
schema: {}
|
||||
security:
|
||||
- Jwt:
|
||||
- users.read
|
||||
summary: Get user
|
||||
tags:
|
||||
- users
|
||||
/users/me:
|
||||
delete:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Delete your account and all your sessions
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.User'
|
||||
security:
|
||||
- Jwt: []
|
||||
summary: Delete self
|
||||
tags:
|
||||
- users
|
||||
get:
|
||||
description: Get informations about the currently connected user
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.User'
|
||||
"401":
|
||||
description: Missing jwt token
|
||||
schema: {}
|
||||
"403":
|
||||
description: Invalid jwt token (or expired)
|
||||
schema: {}
|
||||
security:
|
||||
- Jwt: []
|
||||
summary: Get me
|
||||
tags:
|
||||
- users
|
||||
securityDefinitions:
|
||||
Jwt:
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
Token:
|
||||
in: header
|
||||
name: Authorization
|
||||
type: apiKey
|
||||
swagger: "2.0"
|
14
auth/go.mod
14
auth/go.mod
@ -8,24 +8,22 @@ require (
|
||||
github.com/alexedwards/argon2id v1.0.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.7.3
|
||||
github.com/labstack/echo-jwt/v4 v4.2.0
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
github.com/labstack/echo-jwt/v4 v4.3.1
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/lestrrat-go/jwx v1.2.30
|
||||
github.com/otaxhu/problem v1.4.0
|
||||
github.com/swaggo/echo-swagger v1.4.1
|
||||
github.com/swaggo/swag v1.16.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/otaxhu/type-mismatch-encoding v0.0.0-20241118152201-1861af90dd01 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
)
|
||||
|
||||
@ -39,7 +37,7 @@ require (
|
||||
github.com/go-openapi/swag v0.23.1 // 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.25.0
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@ -58,7 +56,7 @@ require (
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
|
12
auth/go.sum
12
auth/go.sum
@ -11,6 +11,8 @@ 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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8=
|
||||
github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
@ -47,8 +49,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
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/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
@ -70,6 +76,8 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo=
|
||||
github.com/jackc/pgx/v5 v5.7.3/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
|
||||
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
@ -80,6 +88,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
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-jwt/v4 v4.3.1 h1:d8+/qf8nx7RxeL46LtoIwHJsH2PNN8xXCQ/jDianycE=
|
||||
github.com/labstack/echo-jwt/v4 v4.3.1/go.mod h1:yJi83kN8S/5vePVPd+7ID75P4PqPNVRs2HVeuvYJH00=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
@ -171,6 +181,8 @@ 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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
80
auth/jwt.go
80
auth/jwt.go
@ -15,7 +15,7 @@ import (
|
||||
|
||||
type Jwt struct {
|
||||
// The jwt token you can use for all authorized call to either keibi or other services.
|
||||
Token string `json:"token"`
|
||||
Token *string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"`
|
||||
}
|
||||
|
||||
// @Summary Get JWT
|
||||
@ -24,22 +24,64 @@ type Jwt struct {
|
||||
// @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)"
|
||||
// @Failure 403 {object} KError "Invalid session token (or expired)"
|
||||
// @Header 200 {string} Authorization "Jwt (same value as the returned token)"
|
||||
// @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 "):]
|
||||
var jwt *string
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
jwt = h.createGuestJwt()
|
||||
} else {
|
||||
token := auth[len("Bearer "):]
|
||||
|
||||
tkn, err := h.createJwt(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jwt = &tkn
|
||||
}
|
||||
|
||||
if jwt != nil {
|
||||
c.Response().Header().Add("Authorization", fmt.Sprintf("Bearer %s", *jwt))
|
||||
}
|
||||
return c.JSON(http.StatusOK, Jwt{
|
||||
Token: jwt,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handler) createGuestJwt() *string {
|
||||
if h.config.GuestClaims == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
claims := maps.Clone(h.config.GuestClaims)
|
||||
claims["username"] = "guest"
|
||||
claims["sub"] = "guest"
|
||||
claims["sid"] = "guest"
|
||||
claims["iss"] = h.config.PublicUrl
|
||||
claims["iat"] = &jwt.NumericDate{
|
||||
Time: time.Now().UTC(),
|
||||
}
|
||||
claims["exp"] = &jwt.NumericDate{
|
||||
Time: time.Now().UTC().Add(time.Hour),
|
||||
}
|
||||
jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
t, err := jwt.SignedString(h.config.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &t
|
||||
}
|
||||
|
||||
func (h *Handler) createJwt(token string) (string, error) {
|
||||
session, err := h.db.GetUserFromToken(context.Background(), token)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Invalid token")
|
||||
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")
|
||||
return "", echo.NewHTTPError(http.StatusForbidden, "Token has expired")
|
||||
}
|
||||
|
||||
go func() {
|
||||
@ -61,19 +103,27 @@ func (h *Handler) CreateJwt(c echo.Context) error {
|
||||
jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
||||
t, err := jwt.SignedString(h.config.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// only used for the swagger doc
|
||||
type JwkSet struct {
|
||||
Keys []struct {
|
||||
E string `json:"e" example:"AQAB"`
|
||||
KeyOps []string `json:"key_ops" example:"[verify]"`
|
||||
Kty string `json:"kty" example:"RSA"`
|
||||
N string `json:"n" example:"oBcXcJUR-Sb8_b4qIj28LRAPxdF_6odRr52K5-ymiEkR2DOlEuXBtM-biWxPESW-U-zhfHzdVLf6ioy5xL0bJTh8BMIorkrDliN3vb81jCvyOMgZ7ATMJpMAQMmSDN7sL3U45r22FaoQufCJMQHmUsZPecdQSgj2aFBiRXxsLleYlSezdBVT_gKH-coqeYXSC_hk-ezSq4aDZ10BlDnZ-FA7-ES3T7nBmJEAU7KDAGeSvbYAfYimOW0r-Vc0xQNuwGCfzZtSexKXDbYbNwOVo3SjfCabq-gMfap_owcHbKicGBZu1LDlh7CpkmLQf_kv6GihM2LWFFh6Vwg2cltiwF22EIPlUDtYTkUR0qRkdNJaNkwV5Vv_6r3pzSmu5ovRriKtlrvJMjlTnLb4_ltsge3fw5Z34cJrsp094FbUc2O6Or4FGEXUldieJCnVRhs2_h6SDcmeMXs1zfvE5GlDnq8tZV6WMJ5Sb4jNO7rs_hTkr23_E6mVg-DdtozGfqzRzhIjPym6D_jVfR6dZv5W0sKwOHRmT7nYq-C7b2sAwmNNII296M4Rq-jn0b5pgSeMDYbIpbIA4thU8LYU0lBZp_ZVwWKG1RFZDxz3k9O5UVth2kTpTWlwn0hB1aAvgXHo6in1CScITGA72p73RbDieNnLFaCK4xUVstkWAKLqPxs"`
|
||||
Use string `json:"use" example:"sig"`
|
||||
}
|
||||
c.Response().Header().Add("Authorization", fmt.Sprintf("Bearer %s", t))
|
||||
return c.JSON(http.StatusOK, Jwt{
|
||||
Token: t,
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Jwks
|
||||
// @Description Get the jwks info, used to validate jwts.
|
||||
// @Tags jwt
|
||||
// @Produce json
|
||||
// @Success 200 {object} jwk.Key
|
||||
// @Success 200 {object} JwkSet "OK"
|
||||
// @Router /.well-known/jwks.json [get]
|
||||
func (h *Handler) GetJwks(c echo.Context) error {
|
||||
key, err := jwk.New(h.config.JwtPublicKey)
|
||||
|
7
auth/kerror.go
Normal file
7
auth/kerror.go
Normal file
@ -0,0 +1,7 @@
|
||||
package main
|
||||
|
||||
type KError struct {
|
||||
Status int `json:"status" example:"404"`
|
||||
Message string `json:"message" example:"No user found with this id"`
|
||||
Details any `json:"details"`
|
||||
}
|
44
auth/main.go
44
auth/main.go
@ -2,13 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/otaxhu/problem"
|
||||
"github.com/zoriya/kyoo/keibi/dbc"
|
||||
_ "github.com/zoriya/kyoo/keibi/docs"
|
||||
|
||||
@ -31,12 +32,19 @@ func ErrorHandler(err error, c echo.Context) {
|
||||
if he, ok := err.(*echo.HTTPError); ok {
|
||||
code = he.Code
|
||||
message = fmt.Sprint(he.Message)
|
||||
|
||||
if message == "missing or malformed jwt" {
|
||||
code = http.StatusUnauthorized
|
||||
}
|
||||
} else {
|
||||
c.Logger().Error(err)
|
||||
}
|
||||
|
||||
ret := problem.NewMap(code, message)
|
||||
c.JSON(code, ret)
|
||||
c.JSON(code, KError{
|
||||
Status: code,
|
||||
Message: message,
|
||||
Details: nil,
|
||||
})
|
||||
}
|
||||
|
||||
type Validator struct {
|
||||
@ -121,6 +129,35 @@ type Handler struct {
|
||||
config *Configuration
|
||||
}
|
||||
|
||||
func (h *Handler) TokenToJwt(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
auth := c.Request().Header.Get("Authorization")
|
||||
var jwt *string
|
||||
|
||||
if auth == "" || !strings.HasPrefix(auth, "Bearer ") {
|
||||
jwt = h.createGuestJwt()
|
||||
} else {
|
||||
token := auth[len("Bearer "):]
|
||||
// this is only used to check if it is a session token or a jwt
|
||||
_, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
tkn, err := h.createJwt(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jwt = &tkn
|
||||
}
|
||||
|
||||
if jwt != nil {
|
||||
c.Request().Header.Set("Authorization", *jwt)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// @title Keibi - Kyoo's auth
|
||||
// @version 1.0
|
||||
// @description Auth system made for kyoo.
|
||||
@ -165,6 +202,7 @@ func main() {
|
||||
|
||||
g := e.Group(conf.Prefix)
|
||||
r := e.Group(conf.Prefix)
|
||||
r.Use(h.TokenToJwt)
|
||||
r.Use(echojwt.WithConfig(echojwt.Config{
|
||||
SigningMethod: "RS256",
|
||||
SigningKey: h.config.JwtPublicKey,
|
||||
|
28
auth/page.go
Normal file
28
auth/page.go
Normal file
@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import "net/url"
|
||||
|
||||
type Page[T any] struct {
|
||||
Items []T `json:"items"`
|
||||
This string `json:"this" example:"https://kyoo.zoriya.dev/auth/users"`
|
||||
Next *string `json:"next" example:"https://kyoo.zoriya.dev/auth/users?after=aoeusth"`
|
||||
}
|
||||
|
||||
func NewPage(items []User, url *url.URL, limit int32) Page[User] {
|
||||
this := url.String()
|
||||
|
||||
var next *string
|
||||
if len(items) == int(limit) && limit > 0 {
|
||||
query := url.Query()
|
||||
query.Set("after", items[len(items)-1].Id.String())
|
||||
url.RawQuery = query.Encode()
|
||||
nextU := url.String()
|
||||
next = &nextU
|
||||
}
|
||||
|
||||
return Page[User]{
|
||||
Items: items,
|
||||
This: this,
|
||||
Next: next,
|
||||
}
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
*** Settings ***
|
||||
Documentation Common things to handle rest requests
|
||||
|
||||
Library REST http://localhost:4568
|
||||
|
||||
|
||||
*** 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": ""}
|
@ -1,36 +0,0 @@
|
||||
*** 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
|
@ -1,33 +0,0 @@
|
||||
*** 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
|
@ -18,13 +18,18 @@ import (
|
||||
|
||||
type Session struct {
|
||||
// Unique id of this session. Can be used for calls to DELETE
|
||||
Id uuid.UUID `json:"id"`
|
||||
Id uuid.UUID `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"`
|
||||
// When was the session first opened
|
||||
CreatedDate time.Time `json:"createdDate"`
|
||||
CreatedDate time.Time `json:"createdDate" example:"2025-03-29T18:20:05.267Z"`
|
||||
// Last date this session was used to access a service.
|
||||
LastUsed time.Time `json:"lastUsed"`
|
||||
LastUsed time.Time `json:"lastUsed" example:"2025-03-29T18:20:05.267Z"`
|
||||
// Device that created the session.
|
||||
Device *string `json:"device"`
|
||||
Device *string `json:"device" example:"Web - Firefox"`
|
||||
}
|
||||
|
||||
type SessionWToken struct {
|
||||
Session
|
||||
Token string `json:"token" example:"lyHzTYm9yi+pkEv3m2tamAeeK7Dj7N3QRP7xv7dPU5q9MAe8tU4ySwYczE0RaMr4fijsA=="`
|
||||
}
|
||||
|
||||
func MapSession(ses *dbc.Session) Session {
|
||||
@ -36,11 +41,23 @@ func MapSession(ses *dbc.Session) Session {
|
||||
}
|
||||
}
|
||||
|
||||
func MapSessionToken(ses *dbc.Session) SessionWToken {
|
||||
return SessionWToken{
|
||||
Session: Session{
|
||||
Id: ses.Id,
|
||||
CreatedDate: ses.CreatedDate,
|
||||
LastUsed: ses.LastUsed,
|
||||
Device: ses.Device,
|
||||
},
|
||||
Token: ses.Token,
|
||||
}
|
||||
}
|
||||
|
||||
type LoginDto struct {
|
||||
// Either the email or the username.
|
||||
Login string `json:"login" validate:"required"`
|
||||
Login string `json:"login" validate:"required" example:"zoriya"`
|
||||
// Password of the account.
|
||||
Password string `json:"password" validate:"required"`
|
||||
Password string `json:"password" validate:"required" example:"password1234"`
|
||||
}
|
||||
|
||||
// @Summary Login
|
||||
@ -48,19 +65,18 @@ type LoginDto struct {
|
||||
// @Tags sessions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param device query string false "The device the created session will be used on"
|
||||
// @Param device query string false "The device the created session will be used on" example(android tv)
|
||||
// @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)"
|
||||
// @Success 201 {object} SessionWToken
|
||||
// @Failure 403 {object} KError "Invalid password"
|
||||
// @Failure 404 {object} KError "Account does not exists"
|
||||
// @Failure 422 {object} KError "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())
|
||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if err = c.Validate(&req); err != nil {
|
||||
return err
|
||||
@ -102,7 +118,7 @@ func (h *Handler) createSession(c echo.Context, user *User) error {
|
||||
}
|
||||
|
||||
session, err := h.db.CreateSession(ctx, dbc.CreateSessionParams{
|
||||
Token: base64.StdEncoding.EncodeToString(id),
|
||||
Token: base64.RawURLEncoding.EncodeToString(id),
|
||||
UserPk: user.Pk,
|
||||
Device: device,
|
||||
})
|
||||
@ -117,13 +133,9 @@ func (h *Handler) createSession(c echo.Context, user *User) error {
|
||||
// @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]
|
||||
// @Failure 401 {object} KError "Missing jwt token"
|
||||
// @Failure 403 {object} KError "Invalid jwt token (or expired)"
|
||||
// @Router /sessions/current [delete]
|
||||
func (h *Handler) Logout(c echo.Context) error {
|
||||
uid, err := GetCurrentUserId(c)
|
||||
@ -135,13 +147,13 @@ func (h *Handler) Logout(c echo.Context) error {
|
||||
if session == "current" {
|
||||
sid, ok := c.Get("user").(*jwt.Token).Claims.(jwt.MapClaims)["sid"]
|
||||
if !ok {
|
||||
return echo.NewHTTPError(400, "Missing session id")
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Missing session id")
|
||||
}
|
||||
session = sid.(string)
|
||||
}
|
||||
sid, err := uuid.Parse(session)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(400, "Invalid session id")
|
||||
return echo.NewHTTPError(422, "Invalid session id")
|
||||
}
|
||||
|
||||
ret, err := h.db.DeleteSession(context.Background(), dbc.DeleteSessionParams{
|
||||
@ -155,3 +167,15 @@ func (h *Handler) Logout(c echo.Context) error {
|
||||
}
|
||||
return c.JSON(200, MapSession(&ret))
|
||||
}
|
||||
|
||||
// @Summary Delete other session
|
||||
// @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) Example(e05089d6-9179-4b5b-a63e-94dd5fc2a397)
|
||||
// @Success 200 {object} Session
|
||||
// @Failure 404 {object} KError "Session not found with specified id (if not using the /current route)"
|
||||
// @Failure 422 {object} KError "Invalid session id"
|
||||
// @Router /sessions/{id} [delete]
|
||||
func DocOnly() {}
|
||||
|
@ -1,5 +0,0 @@
|
||||
begin;
|
||||
|
||||
drop table config;
|
||||
|
||||
commit;
|
@ -1,8 +0,0 @@
|
||||
begin;
|
||||
|
||||
create table config(
|
||||
key varchar(256) not null primary key,
|
||||
value text not null
|
||||
);
|
||||
|
||||
commit;
|
@ -1,21 +0,0 @@
|
||||
-- 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
|
||||
*;
|
||||
|
@ -51,7 +51,15 @@ where
|
||||
|
||||
-- name: CreateUser :one
|
||||
insert into users(username, email, password, claims)
|
||||
values ($1, $2, $3, $4)
|
||||
values ($1, $2, $3, case when not exists (
|
||||
select
|
||||
*
|
||||
from
|
||||
users) then
|
||||
sqlc.arg(first_claims)::jsonb
|
||||
else
|
||||
sqlc.arg(claims)::jsonb
|
||||
end)
|
||||
returning
|
||||
*;
|
||||
|
||||
|
@ -27,6 +27,9 @@ sql:
|
||||
go_type:
|
||||
import: "github.com/google/uuid"
|
||||
type: "UUID"
|
||||
- db_type: "jsonb"
|
||||
go_type:
|
||||
type: "interface{}"
|
||||
- column: "users.claims"
|
||||
go_type:
|
||||
import: "github.com/golang-jwt/jwt/v5"
|
||||
|
19
auth/tests/basic.hurl
Normal file
19
auth/tests/basic.hurl
Normal file
@ -0,0 +1,19 @@
|
||||
# Bad Account (login fails if user does not exist)
|
||||
POST {{host}}/sessions
|
||||
{
|
||||
"login": "i-don-t-exist",
|
||||
"password": "pass"
|
||||
}
|
||||
HTTP 404
|
||||
|
||||
# Invalid username
|
||||
POST {{host}}/sessions
|
||||
{
|
||||
"login": "invalid-username-user",
|
||||
"password": "pass"
|
||||
}
|
||||
HTTP 404
|
||||
|
||||
# Me cant be accessed without an account
|
||||
GET {{host}}/users/me
|
||||
HTTP 401
|
64
auth/tests/invalid-password.hurl
Normal file
64
auth/tests/invalid-password.hurl
Normal file
@ -0,0 +1,64 @@
|
||||
# Register a user for invalid password test
|
||||
POST {{host}}/users
|
||||
{
|
||||
"username": "login-user",
|
||||
"password": "password-login-user",
|
||||
"email": "invalid-password-user@zoriya.dev"
|
||||
}
|
||||
HTTP 201
|
||||
[Captures]
|
||||
token: jsonpath "$.token"
|
||||
|
||||
GET {{host}}/jwt
|
||||
Authorization: Bearer {{token}}
|
||||
HTTP 200
|
||||
[Captures]
|
||||
jwt: jsonpath "$.token"
|
||||
|
||||
GET {{host}}/users/me
|
||||
Authorization: Bearer {{jwt}}
|
||||
HTTP 200
|
||||
[Captures]
|
||||
register_info: body
|
||||
[Asserts]
|
||||
jsonpath "$.username" == "login-user"
|
||||
|
||||
DELETE {{host}}/sessions/current
|
||||
Authorization: Bearer {{jwt}}
|
||||
HTTP 200
|
||||
|
||||
# Ensure we can login again & /users/me is the same
|
||||
POST {{host}}/sessions
|
||||
{
|
||||
"login": "login-user",
|
||||
"password": "password-login-user"
|
||||
}
|
||||
HTTP 201
|
||||
[Captures]
|
||||
token: jsonpath "$.token"
|
||||
|
||||
GET {{host}}/jwt
|
||||
Authorization: Bearer {{token}}
|
||||
HTTP 200
|
||||
[Captures]
|
||||
jwt: jsonpath "$.token"
|
||||
|
||||
GET {{host}}/users/me
|
||||
Authorization: Bearer {{jwt}}
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
jsonpath "$.username" == "login-user"
|
||||
body == {{register_info}}
|
||||
|
||||
|
||||
# Invalid password login
|
||||
POST {{host}}/sessions
|
||||
{
|
||||
"login": "login-user",
|
||||
"password": "pass-invalid"
|
||||
}
|
||||
HTTP 403
|
||||
|
||||
DELETE {{host}}/users/me
|
||||
Authorization: Bearer {{jwt}}
|
||||
HTTP 200
|
41
auth/tests/users.hurl
Normal file
41
auth/tests/users.hurl
Normal file
@ -0,0 +1,41 @@
|
||||
# Setup
|
||||
POST {{host}}/users
|
||||
{
|
||||
"username": "user-1",
|
||||
"password": "password-user-1",
|
||||
"email": "user-1@zoriya.dev"
|
||||
}
|
||||
HTTP 201
|
||||
[Captures]
|
||||
token: jsonpath "$.token"
|
||||
|
||||
GET {{host}}/jwt
|
||||
Authorization: Bearer {{token}}
|
||||
HTTP 200
|
||||
[Captures]
|
||||
jwt: jsonpath "$.token"
|
||||
|
||||
|
||||
# Duplicates usernames
|
||||
POST {{host}}/users
|
||||
{
|
||||
"username": "user-1",
|
||||
"password": "password-user-duplicate",
|
||||
"email": "user-duplicate@zoriya.dev"
|
||||
}
|
||||
HTTP 409
|
||||
|
||||
|
||||
# Duplicates email
|
||||
POST {{host}}/users
|
||||
{
|
||||
"username": "user-duplicate",
|
||||
"password": "pass",
|
||||
"email": "user-1@zoriya.dev"
|
||||
}
|
||||
HTTP 409
|
||||
|
||||
|
||||
DELETE {{host}}/users/me
|
||||
Authorization: Bearer {{jwt}}
|
||||
HTTP 200
|
@ -18,37 +18,37 @@ type User struct {
|
||||
// Primary key in database
|
||||
Pk int32 `json:"-"`
|
||||
// Id of the user.
|
||||
Id uuid.UUID `json:"id"`
|
||||
Id uuid.UUID `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"`
|
||||
// Username of the user. Can be used as a login.
|
||||
Username string `json:"username"`
|
||||
Username string `json:"username" example:"zoriya"`
|
||||
// Email of the user. Can be used as a login.
|
||||
Email string `json:"email" format:"email"`
|
||||
Email string `json:"email" format:"email" example:"kyoo@zoriya.dev"`
|
||||
// When was this account created?
|
||||
CreatedDate time.Time `json:"createdDate"`
|
||||
CreatedDate time.Time `json:"createdDate" example:"2025-03-29T18:20:05.267Z"`
|
||||
// When was the last time this account made any authorized request?
|
||||
LastSeen time.Time `json:"lastSeen"`
|
||||
LastSeen time.Time `json:"lastSeen" example:"2025-03-29T18:20:05.267Z"`
|
||||
// List of custom claims JWT created via get /jwt will have
|
||||
Claims jwt.MapClaims `json:"claims"`
|
||||
Claims jwt.MapClaims `json:"claims" example:"isAdmin: true"`
|
||||
// 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"`
|
||||
Id string `json:"id" example:"e05089d6-9179-4b5b-a63e-94dd5fc2a397"`
|
||||
// Username of the user on the external service.
|
||||
Username string `json:"username"`
|
||||
Username string `json:"username" example:"zoriya"`
|
||||
// Link to the profile of the user on the external service. Null if unknown or irrelevant.
|
||||
ProfileUrl *string `json:"profileUrl" format:"url"`
|
||||
ProfileUrl *string `json:"profileUrl" format:"url" example:"https://myanimelist.net/profile/zoriya"`
|
||||
}
|
||||
|
||||
type RegisterDto struct {
|
||||
// Username of the new account, can't contain @ signs. Can be used for login.
|
||||
Username string `json:"username" validate:"required,excludes=@"`
|
||||
Username string `json:"username" validate:"required,excludes=@" example:"zoriya"`
|
||||
// Valid email that could be used for forgotten password requests. Can be used for login.
|
||||
Email string `json:"email" validate:"required,email" format:"email"`
|
||||
Email string `json:"email" validate:"required,email" format:"email" example:"kyoo@zoriya.dev"`
|
||||
// Password to use.
|
||||
Password string `json:"password" validate:"required"`
|
||||
Password string `json:"password" validate:"required" example:"password1234"`
|
||||
}
|
||||
|
||||
func MapDbUser(user *dbc.User) User {
|
||||
@ -78,9 +78,9 @@ func MapOidc(oidc *dbc.GetUserRow) OidcHandle {
|
||||
// @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"
|
||||
// @Param after query string false "used for pagination."
|
||||
// @Success 200 {object} Page[User]
|
||||
// @Failure 422 {object} KError "Invalid after id"
|
||||
// @Router /users [get]
|
||||
func (h *Handler) ListUsers(c echo.Context) error {
|
||||
err := CheckPermissions(c, []string{"user.read"})
|
||||
@ -90,7 +90,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
||||
|
||||
ctx := context.Background()
|
||||
limit := int32(20)
|
||||
id := c.Param("afterId")
|
||||
id := c.Param("after")
|
||||
|
||||
var users []dbc.User
|
||||
if id == "" {
|
||||
@ -98,7 +98,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
||||
} else {
|
||||
uid, uerr := uuid.Parse(id)
|
||||
if uerr != nil {
|
||||
return echo.NewHTTPError(400, "Invalid `afterId` parameter, uuid was expected")
|
||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, "Invalid `after` parameter, uuid was expected")
|
||||
}
|
||||
users, err = h.db.GetAllUsersAfter(ctx, dbc.GetAllUsersAfterParams{
|
||||
Limit: limit,
|
||||
@ -114,8 +114,7 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
||||
for _, user := range users {
|
||||
ret = append(ret, MapDbUser(&user))
|
||||
}
|
||||
// TODO: switch to a Page
|
||||
return c.JSON(200, ret)
|
||||
return c.JSON(200, NewPage(ret, c.Request().URL, limit))
|
||||
}
|
||||
|
||||
// @Summary Get user
|
||||
@ -125,7 +124,8 @@ func (h *Handler) ListUsers(c echo.Context) error {
|
||||
// @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"
|
||||
// @Failure 404 {object} KError "No user with the given id found"
|
||||
// @Failure 422 {object} KError "Invalid id (not a uuid)"
|
||||
// @Router /users/{id} [get]
|
||||
func (h *Handler) GetUser(c echo.Context) error {
|
||||
err := CheckPermissions(c, []string{"user.read"})
|
||||
@ -135,7 +135,7 @@ func (h *Handler) GetUser(c echo.Context) error {
|
||||
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(400, "Invalid id")
|
||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, "Invalid id")
|
||||
}
|
||||
dbuser, err := h.db.GetUser(context.Background(), id)
|
||||
if err != nil {
|
||||
@ -158,8 +158,8 @@ func (h *Handler) GetUser(c echo.Context) error {
|
||||
// @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)"
|
||||
// @Failure 401 {object} KError "Missing jwt token"
|
||||
// @Failure 403 {object} KError "Invalid jwt token (or expired)"
|
||||
// @Router /users/me [get]
|
||||
func (h *Handler) GetMe(c echo.Context) error {
|
||||
id, err := GetCurrentUserId(c)
|
||||
@ -186,17 +186,17 @@ func (h *Handler) GetMe(c echo.Context) error {
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param device query string false "The device the created session will be used on"
|
||||
// @Param device query string false "The device the created session will be used on" Example(android)
|
||||
// @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"
|
||||
// @Success 201 {object} SessionWToken
|
||||
// @Success 409 {object} KError "Duplicated email or username"
|
||||
// @Failure 422 {object} KError "Invalid register body"
|
||||
// @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())
|
||||
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if err = c.Validate(&req); err != nil {
|
||||
return err
|
||||
@ -208,10 +208,11 @@ func (h *Handler) Register(c echo.Context) error {
|
||||
}
|
||||
|
||||
duser, err := h.db.CreateUser(context.Background(), dbc.CreateUserParams{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: &pass,
|
||||
Claims: h.config.DefaultClaims,
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: &pass,
|
||||
Claims: h.config.DefaultClaims,
|
||||
FirstClaims: h.config.FirstUserClaims,
|
||||
})
|
||||
if ErrIs(err, pgerrcode.UniqueViolation) {
|
||||
return echo.NewHTTPError(409, "Email or username already taken")
|
||||
@ -230,8 +231,8 @@ func (h *Handler) Register(c echo.Context) error {
|
||||
// @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"
|
||||
// @Failure 404 {object} KError "Invalid id format"
|
||||
// @Failure 404 {object} KError "Invalid user id"
|
||||
// @Router /users/{id} [delete]
|
||||
func (h *Handler) DeleteUser(c echo.Context) error {
|
||||
uid, err := uuid.Parse(c.Param("id"))
|
||||
|
@ -19,7 +19,7 @@ func GetCurrentUserId(c echo.Context) (uuid.UUID, error) {
|
||||
}
|
||||
sub, err := user.Claims.GetSubject()
|
||||
if err != nil {
|
||||
return uuid.UUID{}, echo.NewHTTPError(403, "Could not retrive subject")
|
||||
return uuid.UUID{}, echo.NewHTTPError(403, "Could not retrieve subject")
|
||||
}
|
||||
ret, err := uuid.Parse(sub)
|
||||
if err != nil {
|
||||
|
@ -60,8 +60,7 @@ services:
|
||||
- ./auth:/app
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.auth.rule=PathPrefix(`/auth/`)"
|
||||
- "traefik.http.routers.auth.rule=PathPrefix(`/.well-known/`)"
|
||||
- "traefik.http.routers.auth.rule=PathPrefix(`/auth/`) || PathPrefix(`/.well-known/`)"
|
||||
|
||||
api:
|
||||
build:
|
||||
@ -71,8 +70,6 @@ services:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
auth:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./api:/app
|
||||
- /app/node_modules
|
||||
@ -80,11 +77,12 @@ services:
|
||||
- "3567:3567"
|
||||
environment:
|
||||
- KYOO_PREFIX=/api
|
||||
- JWT_ISSUER=${PUBLIC_URL}
|
||||
env_file:
|
||||
- ./.env
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=PathPrefix(`/api/`)"
|
||||
- "traefik.http.routers.api.rule=PathPrefix(`/api/`) || PathPrefix(`/swagger`)"
|
||||
- "traefik.http.routers.api.middlewares=phantom-token"
|
||||
- "traefik.http.middlewares.phantom-token.forwardauth.address=http://auth:4568/auth/jwt"
|
||||
- "traefik.http.middlewares.phantom-token.forwardauth.authRequestHeaders=Authorization,X-Api-Key"
|
||||
|
@ -1,2 +0,0 @@
|
||||
robotframework
|
||||
RESTinstance
|
@ -11,9 +11,6 @@
|
||||
dataclasses-json
|
||||
msgspec
|
||||
langcodes
|
||||
|
||||
# robotframework
|
||||
# restinstance needs to be packaged
|
||||
]);
|
||||
dotnet = with pkgs.dotnetCorePackages;
|
||||
combinePackages [
|
||||
@ -40,11 +37,11 @@ in
|
||||
go-migrate
|
||||
sqlc
|
||||
go-swag
|
||||
robotframework-tidy
|
||||
bun
|
||||
pkg-config
|
||||
node-gyp
|
||||
vips
|
||||
hurl
|
||||
];
|
||||
|
||||
DOTNET_ROOT = "${dotnet}";
|
||||
|
Loading…
x
Reference in New Issue
Block a user