diff --git a/api/.env.example b/api/.env.example index 9f395c93..f7bf9c67 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,6 +1,8 @@ # vi: ft=sh # shellcheck disable=SC2034 +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= diff --git a/api/Dockerfile b/api/Dockerfile index 3e434495..f603f78a 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -24,6 +24,5 @@ WORKDIR /app COPY --from=builder /app/server server ENV NODE_ENV=production -EXPOSE 3000 +EXPOSE 3567 CMD ["./server"] - diff --git a/api/Dockerfile.dev b/api/Dockerfile.dev new file mode 100644 index 00000000..33cc0ec9 --- /dev/null +++ b/api/Dockerfile.dev @@ -0,0 +1,10 @@ +FROM oven/bun AS builder +WORKDIR /app + +COPY package.json bun.lock . +COPY patches patches +RUN bun install --production + +EXPOSE 3567 +CMD ["bun", "dev"] + diff --git a/api/bun.lock b/api/bun.lock index bdb5a132..30156aa0 100644 --- a/api/bun.lock +++ b/api/bun.lock @@ -4,12 +4,12 @@ "": { "name": "api", "dependencies": { - "@elysiajs/jwt": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "blurhash": "^2.0.5", "drizzle-kit": "^0.30.4", "drizzle-orm": "0.39.0", "elysia": "^1.2.23", + "jose": "^6.0.10", "parjs": "^1.3.9", "pg": "^8.13.3", "sharp": "^0.33.5", @@ -27,8 +27,6 @@ "packages": { "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - "@elysiajs/jwt": ["@elysiajs/jwt@1.2.0", "", { "dependencies": { "jose": "^4.14.4" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-5iMoZucIKNAqPKW3n6RBIyCnDWG3kOcqA4WZKtqEff+IjV6AN3dlMSE2XsS0xjIvusLD0UBXS8cxQ9NwIcj6ew=="], - "@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=="], "@emnapi/runtime": ["@emnapi/runtime@1.3.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw=="], @@ -183,7 +181,7 @@ "isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], - "jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], + "jose": ["jose@6.0.10", "", {}, "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw=="], "memoirist": ["memoirist@0.3.0", "", {}, "sha512-wR+4chMgVPq+T6OOsk40u9Wlpw1Pjx66NMNiYxCQQ4EUJ7jDs3D9kTCeKdBOkvAiqXlHLVJlvYL01PvIJ1MPNg=="], diff --git a/api/package.json b/api/package.json index a0fea57d..abecb29f 100644 --- a/api/package.json +++ b/api/package.json @@ -9,12 +9,12 @@ "format": "biome check --write ." }, "dependencies": { - "@elysiajs/jwt": "^1.2.0", "@elysiajs/swagger": "^1.2.2", "blurhash": "^2.0.5", "drizzle-kit": "^0.30.4", "drizzle-orm": "0.39.0", "elysia": "^1.2.23", + "jose": "^6.0.10", "parjs": "^1.3.9", "pg": "^8.13.3", "sharp": "^0.33.5" diff --git a/api/src/auth.ts b/api/src/auth.ts new file mode 100644 index 00000000..398fd1f9 --- /dev/null +++ b/api/src/auth.ts @@ -0,0 +1,47 @@ +import Elysia, { getSchemaValidator, t } from "elysia"; +import { createRemoteJWKSet, jwtVerify } from "jose"; +import { KError } from "./models/error"; + +const jwtSecret = process.env.JWT_SECRET + ? new TextEncoder().encode(process.env.JWT_SECRET) + : null; +const jwks = createRemoteJWKSet( + new URL( + ".well-known/jwks.json", + process.env.AUTH_SERVER ?? "http://auth:4568", + ), +); + +const Jwt = t.Object({ + sub: t.String({ description: "User id" }), + username: t.String(), + sid: t.String({ description: "Session id" }), +}); +const validator = getSchemaValidator(Jwt); + +export const auth = new Elysia({ name: "auth" }) + .guard({ + // Those are not applied for now. See https://github.com/elysiajs/elysia/issues/1139 + detail: { + security: [{ bearer: ["read"] }, { api: ["read"] }], + }, + response: { + 401: { ...KError, description: "" }, + 403: { ...KError, description: "" }, + }, + }) + .macro({ + permissions(perms: string[]) { + return { + resolve: async ({ headers: { authorization }, error }) => { + 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); + // TODO: use perms + return { jwt: validator.Decode(payload) }; + }, + }; + }, + }) + .as("plugin"); diff --git a/api/src/elysia.ts b/api/src/base.ts similarity index 83% rename from api/src/elysia.ts rename to api/src/base.ts index b1075cd5..248ac1a1 100644 --- a/api/src/elysia.ts +++ b/api/src/base.ts @@ -1,4 +1,4 @@ -import { Elysia } from "elysia"; +import { Elysia, t } from "elysia"; import { entriesH } from "./controllers/entries"; import { imagesH } from "./controllers/images"; import { seasonsH } from "./controllers/seasons"; @@ -44,9 +44,14 @@ export const base = new Elysia({ name: "base" }) console.error(code, error); return error; }) + .get("/health", () => ({ status: "healthy" }) as const, { + detail: { description: "Check if the api is healthy." }, + response: { 200: t.Object({ status: t.Literal("healthy") }) }, + }) .as("plugin"); -export const app = new Elysia() +export const prefix = process.env.KYOO_PREFIX ?? ""; +export const app = new Elysia({ prefix }) .use(base) .use(showsH) .use(movies) diff --git a/api/src/controllers/images.ts b/api/src/controllers/images.ts index 4d7e7a27..865af6cc 100644 --- a/api/src/controllers/images.ts +++ b/api/src/controllers/images.ts @@ -2,6 +2,7 @@ import { stat } from "node:fs/promises"; import type { BunFile } from "bun"; import { type SQL, and, eq, sql } from "drizzle-orm"; import Elysia, { type Context, t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { showTranslations, @@ -87,8 +88,8 @@ function getRedirectToImageHandler({ } set.headers["content-language"] = ret.language; return quality - ? redirect(`/images/${ret.image!.id}?quality=${quality}`) - : redirect(`/images/${ret.image!.id}`); + ? redirect(`${prefix}/images/${ret.image!.id}?quality=${quality}`) + : redirect(`${prefix}/images/${ret.image!.id}`); }; } @@ -181,8 +182,8 @@ export const imagesH = new Elysia({ tags: ["images"] }) }); } return quality - ? redirect(`/images/${ret.image!.id}?quality=${quality}`) - : redirect(`/images/${ret.image!.id}`); + ? redirect(`${prefix}/images/${ret.image!.id}?quality=${quality}`) + : redirect(`${prefix}/images/${ret.image!.id}`); }, { detail: { description: "Get the image of a staff member." }, @@ -256,8 +257,8 @@ export const imagesH = new Elysia({ tags: ["images"] }) } set.headers["content-language"] = ret.language; return quality - ? redirect(`/images/${ret.image!.id}?quality=${quality}`) - : redirect(`/images/${ret.image!.id}`); + ? redirect(`${prefix}/images/${ret.image!.id}?quality=${quality}`) + : redirect(`${prefix}/images/${ret.image!.id}`); }, { detail: { description: "Get the logo of a studio." }, diff --git a/api/src/controllers/shows/collections.ts b/api/src/controllers/shows/collections.ts index a845524a..cb5474e6 100644 --- a/api/src/controllers/shows/collections.ts +++ b/api/src/controllers/shows/collections.ts @@ -1,5 +1,6 @@ import { and, eq, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { shows } from "~/db/schema"; import { @@ -113,7 +114,7 @@ export const collections = new Elysia({ status: 404, message: "No collection in the database.", }); - return redirect(`/collections/${serie.slug}`); + return redirect(`${prefix}/collections/${serie.slug}`); }, { detail: { diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts index e1bb197d..829edc7a 100644 --- a/api/src/controllers/shows/movies.ts +++ b/api/src/controllers/shows/movies.ts @@ -1,5 +1,6 @@ import { and, eq, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { shows } from "~/db/schema"; import { KError } from "~/models/error"; @@ -103,7 +104,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) status: 404, message: "No movies in the database.", }); - return redirect(`/movies/${movie.slug}`); + return redirect(`${prefix}/movies/${movie.slug}`); }, { detail: { diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts index 5aad0163..c788efdd 100644 --- a/api/src/controllers/shows/series.ts +++ b/api/src/controllers/shows/series.ts @@ -1,5 +1,6 @@ import { and, eq, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { shows } from "~/db/schema"; import { KError } from "~/models/error"; @@ -45,7 +46,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) if (!ret) { return error(404, { status: 404, - message: "Movie not found", + message: `No serie found with the id or slug: '${id}'.`, }); } if (!ret.language) { @@ -103,7 +104,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) status: 404, message: "No series in the database.", }); - return redirect(`/series/${serie.slug}`); + return redirect(`${prefix}/series/${serie.slug}`); }, { detail: { diff --git a/api/src/controllers/shows/shows.ts b/api/src/controllers/shows/shows.ts index 49b837dd..2b6e5f87 100644 --- a/api/src/controllers/shows/shows.ts +++ b/api/src/controllers/shows/shows.ts @@ -1,5 +1,6 @@ import { and, isNull, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { shows } from "~/db/schema"; import { KError } from "~/models/error"; @@ -31,7 +32,7 @@ export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] }) status: 404, message: "No shows in the database.", }); - return redirect(`/${show.kind}s/${show.slug}`); + return redirect(`${prefix}/${show.kind}s/${show.slug}`); }, { detail: { diff --git a/api/src/controllers/staff.ts b/api/src/controllers/staff.ts index 56c1fa85..4160d39e 100644 --- a/api/src/controllers/staff.ts +++ b/api/src/controllers/staff.ts @@ -1,5 +1,6 @@ import { type SQL, and, eq, sql } from "drizzle-orm"; import Elysia, { t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { showTranslations, shows } from "~/db/schema"; import { roles, staff } from "~/db/schema/staff"; @@ -160,7 +161,7 @@ export const staffH = new Elysia({ tags: ["staff"] }) status: 404, message: "No staff in the database.", }); - return redirect(`/staff/${member.slug}`); + return redirect(`${prefix}/staff/${member.slug}`); }, { detail: { diff --git a/api/src/controllers/studios.ts b/api/src/controllers/studios.ts index ea9bac91..65ce97e9 100644 --- a/api/src/controllers/studios.ts +++ b/api/src/controllers/studios.ts @@ -1,5 +1,6 @@ import { type SQL, and, eq, exists, sql } from "drizzle-orm"; import Elysia, { t } from "elysia"; +import { prefix } from "~/base"; import { db } from "~/db"; import { showStudioJoin, @@ -200,7 +201,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) status: 404, message: "No studios in the database.", }); - return redirect(`/studios/${studio.slug}`); + return redirect(`${prefix}/studios/${studio.slug}`); }, { detail: { diff --git a/api/src/index.ts b/api/src/index.ts index c1f02370..b029433f 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,34 +1,15 @@ -import jwt from "@elysiajs/jwt"; import { swagger } from "@elysiajs/swagger"; +import { app } from "./base"; import { processImages } from "./controllers/seed/images"; import { migrate } from "./db"; -import { app } from "./elysia"; import { comment } from "./utils"; await migrate(); -let secret = process.env.JWT_SECRET; -if (!secret) { - const auth = process.env.AUTH_SERVER ?? "http://auth:4568"; - try { - const ret = await fetch(`${auth}/info`); - const info = await ret.json(); - secret = info.publicKey; - } catch (error) { - console.error(`Can't access auth server at ${auth}:\n${error}`); - } -} - -if (!secret) { - console.error("Missing jwt secret or auth server. exiting"); - process.exit(1); -} - // run image processor task in background processImages(); app - .use(jwt({ secret })) .use( swagger({ documentation: { @@ -74,9 +55,23 @@ app description: "Routes about images: posters, thumbnails...", }, ], + components: { + securitySchemes: { + bearer: { + type: "http", + scheme: "bearer", + bearerFormat: "opaque", + }, + api: { + type: "apiKey", + in: "header", + name: "X-API-KEY", + }, + }, + }, }, }), ) - .listen(3000); + .listen(3567); console.log(`Api running at ${app.server?.hostname}:${app.server?.port}`); diff --git a/api/tests/helpers/index.ts b/api/tests/helpers/index.ts index d7dd1617..e2898d52 100644 --- a/api/tests/helpers/index.ts +++ b/api/tests/helpers/index.ts @@ -4,4 +4,4 @@ export * from "./studio-helper"; export * from "./staff-helper"; export * from "./videos-helper"; -export * from "~/elysia"; +export * from "~/base"; diff --git a/api/tests/helpers/movies-helper.ts b/api/tests/helpers/movies-helper.ts index 8ba77603..c7ddba29 100644 --- a/api/tests/helpers/movies-helper.ts +++ b/api/tests/helpers/movies-helper.ts @@ -1,5 +1,5 @@ import { buildUrl } from "tests/utils"; -import { app } from "~/elysia"; +import { app } from "~/base"; import type { SeedMovie } from "~/models/movie"; export const getMovie = async ( diff --git a/api/tests/helpers/series-helper.ts b/api/tests/helpers/series-helper.ts index b180891b..776c09eb 100644 --- a/api/tests/helpers/series-helper.ts +++ b/api/tests/helpers/series-helper.ts @@ -1,5 +1,5 @@ import { buildUrl } from "tests/utils"; -import { app } from "~/elysia"; +import { app } from "~/base"; import type { SeedSerie } from "~/models/serie"; export const createSerie = async (serie: SeedSerie) => { diff --git a/api/tests/helpers/staff-helper.ts b/api/tests/helpers/staff-helper.ts index 4c790de1..a4a4b99b 100644 --- a/api/tests/helpers/staff-helper.ts +++ b/api/tests/helpers/staff-helper.ts @@ -1,5 +1,5 @@ import { buildUrl } from "tests/utils"; -import { app } from "~/elysia"; +import { app } from "~/base"; export const getStaff = async (id: string, query: {}) => { const resp = await app.handle( diff --git a/api/tests/helpers/studio-helper.ts b/api/tests/helpers/studio-helper.ts index fb9fe255..bc6ccfe8 100644 --- a/api/tests/helpers/studio-helper.ts +++ b/api/tests/helpers/studio-helper.ts @@ -1,5 +1,5 @@ import { buildUrl } from "tests/utils"; -import { app } from "~/elysia"; +import { app } from "~/base"; export const getStudio = async ( id: string, diff --git a/api/tests/helpers/videos-helper.ts b/api/tests/helpers/videos-helper.ts index 5dd0bb70..74232b7c 100644 --- a/api/tests/helpers/videos-helper.ts +++ b/api/tests/helpers/videos-helper.ts @@ -1,5 +1,5 @@ import { buildUrl } from "tests/utils"; -import { app } from "~/elysia"; +import { app } from "~/base"; import type { SeedVideo } from "~/models/video"; export const createVideo = async (video: SeedVideo | SeedVideo[]) => { diff --git a/auth/.env.example b/auth/.env.example index bf462faa..de7c4b08 100644 --- a/auth/.env.example +++ b/auth/.env.example @@ -4,6 +4,9 @@ # http route prefix (will listen to $KEIBI_PREFIX/users for example) KEIBI_PREFIX="" +# 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 + # Database things POSTGRES_USER=kyoo POSTGRES_PASSWORD=password diff --git a/auth/.gitignore b/auth/.gitignore deleted file mode 100644 index cb4266fd..00000000 --- a/auth/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# generated via sqlc -dbc/ -# genereated via swag -docs/ diff --git a/auth/Dockerfile b/auth/Dockerfile index 5295e0c2..35654b31 100644 --- a/auth/Dockerfile +++ b/auth/Dockerfile @@ -23,4 +23,5 @@ USER nonroot:nonroot COPY --from=build /keibi /app/keibi COPY sql ./sql +HEALTHCHECK --interval=30s --retries=15 CMD curl --fail http://localhost:4568$KEIBI_PREFIX/health || exit CMD ["/app/keibi"] diff --git a/auth/Dockerfile.dev b/auth/Dockerfile.dev index 5a7646f0..fbbb7edf 100644 --- a/auth/Dockerfile.dev +++ b/auth/Dockerfile.dev @@ -2,15 +2,10 @@ FROM golang:1.24 AS build WORKDIR /app RUN go install github.com/bokwoon95/wgo@latest -RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest -RUN go install github.com/swaggo/swag/cmd/swag@latest COPY go.mod go.sum ./ RUN go mod download -# COPY sqlc.yaml ./ -# COPY sql/ ./ -# RUN sqlc generate - EXPOSE 4568 +HEALTHCHECK --interval=30s --retries=15 CMD curl --fail http://localhost:4568$KEIBI_PREFIX/health || exit CMD ["wgo", "run", "-race", "."] diff --git a/auth/config.go b/auth/config.go index f49352db..737226da 100644 --- a/auth/config.go +++ b/auth/config.go @@ -17,13 +17,12 @@ type Configuration struct { Prefix string JwtPrivateKey *rsa.PrivateKey JwtPublicKey *rsa.PublicKey - Issuer string + PublicUrl string DefaultClaims jwt.MapClaims ExpirationDelay time.Duration } var DefaultConfig = Configuration{ - Issuer: "kyoo", DefaultClaims: make(jwt.MapClaims), ExpirationDelay: 30 * 24 * time.Hour, } @@ -54,6 +53,7 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) { } } + ret.PublicUrl = os.Getenv("PUBLIC_URL") ret.Prefix = os.Getenv("KEIBI_PREFIX") if ret.JwtPrivateKey == nil { diff --git a/auth/dbc/config.sql.go b/auth/dbc/config.sql.go new file mode 100644 index 00000000..5ec22ed9 --- /dev/null +++ b/auth/dbc/config.sql.go @@ -0,0 +1,73 @@ +// 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 +} diff --git a/auth/dbc/db.go b/auth/dbc/db.go new file mode 100644 index 00000000..babe8e31 --- /dev/null +++ b/auth/dbc/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package dbc + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/auth/dbc/models.go b/auth/dbc/models.go new file mode 100644 index 00000000..122487ae --- /dev/null +++ b/auth/dbc/models.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 + +package dbc + +import ( + "time" + + jwt "github.com/golang-jwt/jwt/v5" + "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"` + Id string `json:"id"` + Username string `json:"username"` + ProfileUrl *string `json:"profileUrl"` + AccessToken *string `json:"accessToken"` + RefreshToken *string `json:"refreshToken"` + ExpireAt *time.Time `json:"expireAt"` +} + +type Session struct { + Pk int32 `json:"pk"` + Id uuid.UUID `json:"id"` + Token string `json:"token"` + UserPk int32 `json:"userPk"` + CreatedDate time.Time `json:"createdDate"` + LastUsed time.Time `json:"lastUsed"` + Device *string `json:"device"` +} + +type User struct { + Pk int32 `json:"pk"` + Id uuid.UUID `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password *string `json:"password"` + Claims jwt.MapClaims `json:"claims"` + CreatedDate time.Time `json:"createdDate"` + LastSeen time.Time `json:"lastSeen"` +} diff --git a/auth/dbc/sessions.sql.go b/auth/dbc/sessions.sql.go new file mode 100644 index 00000000..02509fd9 --- /dev/null +++ b/auth/dbc/sessions.sql.go @@ -0,0 +1,161 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: sessions.sql + +package dbc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createSession = `-- name: CreateSession :one +insert into sessions(token, user_pk, device) + values ($1, $2, $3) +returning + pk, id, token, user_pk, created_date, last_used, device +` + +type CreateSessionParams struct { + Token string `json:"token"` + UserPk int32 `json:"userPk"` + Device *string `json:"device"` +} + +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { + row := q.db.QueryRow(ctx, createSession, arg.Token, arg.UserPk, arg.Device) + var i Session + err := row.Scan( + &i.Pk, + &i.Id, + &i.Token, + &i.UserPk, + &i.CreatedDate, + &i.LastUsed, + &i.Device, + ) + return i, err +} + +const deleteSession = `-- name: DeleteSession :one +delete from sessions as s using users as u +where s.user_pk = u.pk + and s.id = $1 + and u.id = $2 +returning + s.pk, s.id, s.token, s.user_pk, s.created_date, s.last_used, s.device +` + +type DeleteSessionParams struct { + Id uuid.UUID `json:"id"` + UserId uuid.UUID `json:"userId"` +} + +func (q *Queries) DeleteSession(ctx context.Context, arg DeleteSessionParams) (Session, error) { + row := q.db.QueryRow(ctx, deleteSession, arg.Id, arg.UserId) + var i Session + err := row.Scan( + &i.Pk, + &i.Id, + &i.Token, + &i.UserPk, + &i.CreatedDate, + &i.LastUsed, + &i.Device, + ) + return i, err +} + +const getUserFromToken = `-- name: GetUserFromToken :one +select + s.id, + s.last_used, + u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen +from + users as u + inner join sessions as s on u.pk = s.user_pk +where + s.token = $1 +limit 1 +` + +type GetUserFromTokenRow struct { + Id uuid.UUID `json:"id"` + LastUsed time.Time `json:"lastUsed"` + User User `json:"user"` +} + +func (q *Queries) GetUserFromToken(ctx context.Context, token string) (GetUserFromTokenRow, error) { + row := q.db.QueryRow(ctx, getUserFromToken, token) + var i GetUserFromTokenRow + err := row.Scan( + &i.Id, + &i.LastUsed, + &i.User.Pk, + &i.User.Id, + &i.User.Username, + &i.User.Email, + &i.User.Password, + &i.User.Claims, + &i.User.CreatedDate, + &i.User.LastSeen, + ) + return i, err +} + +const getUserSessions = `-- name: GetUserSessions :many +select + s.pk, s.id, s.token, s.user_pk, s.created_date, s.last_used, s.device +from + sessions as s + inner join users as u on u.pk = s.user_pk +where + u.pk = $1 +order by + last_used +` + +func (q *Queries) GetUserSessions(ctx context.Context, pk int32) ([]Session, error) { + rows, err := q.db.Query(ctx, getUserSessions, pk) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Session + for rows.Next() { + var i Session + if err := rows.Scan( + &i.Pk, + &i.Id, + &i.Token, + &i.UserPk, + &i.CreatedDate, + &i.LastUsed, + &i.Device, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const touchSession = `-- name: TouchSession :exec +update + sessions +set + last_used = now()::timestamptz +where + id = $1 +` + +func (q *Queries) TouchSession(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, touchSession, id) + return err +} diff --git a/auth/dbc/users.sql.go b/auth/dbc/users.sql.go new file mode 100644 index 00000000..88360e27 --- /dev/null +++ b/auth/dbc/users.sql.go @@ -0,0 +1,296 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: users.sql + +package dbc + +import ( + "context" + + jwt "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" +) + +const createUser = `-- name: CreateUser :one +insert into users(username, email, password, claims) + values ($1, $2, $3, $4) +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"` +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRow(ctx, createUser, + arg.Username, + arg.Email, + arg.Password, + arg.Claims, + ) + var i User + err := row.Scan( + &i.Pk, + &i.Id, + &i.Username, + &i.Email, + &i.Password, + &i.Claims, + &i.CreatedDate, + &i.LastSeen, + ) + return i, err +} + +const deleteUser = `-- name: DeleteUser :one +delete from users +where id = $1 +returning + pk, id, username, email, password, claims, created_date, last_seen +` + +func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) (User, error) { + row := q.db.QueryRow(ctx, deleteUser, id) + var i User + err := row.Scan( + &i.Pk, + &i.Id, + &i.Username, + &i.Email, + &i.Password, + &i.Claims, + &i.CreatedDate, + &i.LastSeen, + ) + return i, err +} + +const getAllUsers = `-- name: GetAllUsers :many +select + pk, id, username, email, password, claims, created_date, last_seen +from + users +order by + id +limit $1 +` + +func (q *Queries) GetAllUsers(ctx context.Context, limit int32) ([]User, error) { + rows, err := q.db.Query(ctx, getAllUsers, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.Pk, + &i.Id, + &i.Username, + &i.Email, + &i.Password, + &i.Claims, + &i.CreatedDate, + &i.LastSeen, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getAllUsersAfter = `-- name: GetAllUsersAfter :many +select + pk, id, username, email, password, claims, created_date, last_seen +from + users +where + id >= $2 +order by + id +limit $1 +` + +type GetAllUsersAfterParams struct { + Limit int32 `json:"limit"` + AfterId uuid.UUID `json:"afterId"` +} + +func (q *Queries) GetAllUsersAfter(ctx context.Context, arg GetAllUsersAfterParams) ([]User, error) { + rows, err := q.db.Query(ctx, getAllUsersAfter, arg.Limit, arg.AfterId) + if err != nil { + return nil, err + } + defer rows.Close() + var items []User + for rows.Next() { + var i User + if err := rows.Scan( + &i.Pk, + &i.Id, + &i.Username, + &i.Email, + &i.Password, + &i.Claims, + &i.CreatedDate, + &i.LastSeen, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUser = `-- name: GetUser :many +select + u.pk, u.id, u.username, u.email, u.password, u.claims, u.created_date, u.last_seen, + h.provider, + h.id, + h.username, + h.profile_url +from + users as u + left join oidc_handle as h on u.pk = h.user_pk +where + u.id = $1 +` + +type GetUserRow struct { + User User `json:"user"` + Provider *string `json:"provider"` + Id *string `json:"id"` + Username *string `json:"username"` + ProfileUrl *string `json:"profileUrl"` +} + +func (q *Queries) GetUser(ctx context.Context, id uuid.UUID) ([]GetUserRow, error) { + rows, err := q.db.Query(ctx, getUser, id) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetUserRow + for rows.Next() { + var i GetUserRow + if err := rows.Scan( + &i.User.Pk, + &i.User.Id, + &i.User.Username, + &i.User.Email, + &i.User.Password, + &i.User.Claims, + &i.User.CreatedDate, + &i.User.LastSeen, + &i.Provider, + &i.Id, + &i.Username, + &i.ProfileUrl, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getUserByLogin = `-- name: GetUserByLogin :one +select + pk, id, username, email, password, claims, created_date, last_seen +from + users +where + email = $1 + or username = $1 +limit 1 +` + +func (q *Queries) GetUserByLogin(ctx context.Context, login string) (User, error) { + row := q.db.QueryRow(ctx, getUserByLogin, login) + var i User + err := row.Scan( + &i.Pk, + &i.Id, + &i.Username, + &i.Email, + &i.Password, + &i.Claims, + &i.CreatedDate, + &i.LastSeen, + ) + return i, err +} + +const touchUser = `-- name: TouchUser :exec +update + users +set + last_used = now()::timestamptz +where + id = $1 +` + +func (q *Queries) TouchUser(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, touchUser, id) + return err +} + +const updateUser = `-- name: UpdateUser :one +update + users +set + username = $2, + email = $3, + password = $4, + claims = $5 +where + id = $1 +returning + pk, id, username, email, password, claims, created_date, last_seen +` + +type UpdateUserParams struct { + Id uuid.UUID `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password *string `json:"password"` + Claims jwt.MapClaims `json:"claims"` +} + +func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) { + row := q.db.QueryRow(ctx, updateUser, + arg.Id, + arg.Username, + arg.Email, + arg.Password, + arg.Claims, + ) + var i User + err := row.Scan( + &i.Pk, + &i.Id, + &i.Username, + &i.Email, + &i.Password, + &i.Claims, + &i.CreatedDate, + &i.LastSeen, + ) + return i, err +} diff --git a/auth/docs/docs.go b/auth/docs/docs.go new file mode 100644 index 00000000..e3e40a2e --- /dev/null +++ b/auth/docs/docs.go @@ -0,0 +1,650 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Repository", + "url": "https://github.com/zoriya/kyoo" + }, + "license": { + "name": "GPL-3.0", + "url": "https://www.gnu.org/licenses/gpl-3.0.en.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/info": { + "get": { + "description": "Get info like the public key used to sign the jwts.", + "produces": [ + "application/json" + ], + "tags": [ + "jwt" + ], + "summary": "Info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Info" + } + } + } + } + }, + "/jwt": { + "get": { + "security": [ + { + "Token": [] + } + ], + "description": "Convert a session token to a short lived JWT.", + "produces": [ + "application/json" + ], + "tags": [ + "jwt" + ], + "summary": "Get JWT", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Jwt" + } + }, + "401": { + "description": "Missing session token", + "schema": {} + }, + "403": { + "description": "Invalid session token (or expired)", + "schema": {} + } + } + } + }, + "/sessions": { + "post": { + "description": "Login to your account and open a session", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "Login", + "parameters": [ + { + "type": "string", + "description": "The device the created session will be used on", + "name": "device", + "in": "query" + }, + { + "description": "Account informations", + "name": "login", + "in": "body", + "schema": { + "$ref": "#/definitions/main.LoginDto" + } + } + ], + "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": {} + } + } + } + }, + "/sessions/current": { + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete a session and logout", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "Logout", + "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": {} + } + } + } + }, + "/sessions/{id}": { + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete a session and logout", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "Logout", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "The id of the session to delete", + "name": "id", + "in": "path", + "required": true + } + ], + "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": {} + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "Jwt": [ + "users.read" + ] + } + ], + "description": "List all users existing in this instance.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List all users", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "used for pagination.", + "name": "afterId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "400": { + "description": "Invalid after id", + "schema": {} + } + } + }, + "post": { + "description": "Register as a new user and open a session for it", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Register", + "parameters": [ + { + "type": "string", + "description": "The device the created session will be used on", + "name": "device", + "in": "query" + }, + { + "description": "Registration informations", + "name": "user", + "in": "body", + "schema": { + "$ref": "#/definitions/main.RegisterDto" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dbc.Session" + } + }, + "400": { + "description": "Invalid register body", + "schema": {} + }, + "409": { + "description": "Duplicated email or username", + "schema": {} + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Get informations about the currently connected user", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get me", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "401": { + "description": "Missing jwt token", + "schema": {} + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete your account and all your sessions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete self", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "Jwt": [ + "users.read" + ] + } + ], + "description": "Get informations about a user from it's id", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get user", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "The id of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "404": { + "description": "No user with the given id found", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "Jwt": [ + "users.delete" + ] + } + ], + "description": "Delete an account and all it's sessions.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete user", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "User id of the user to delete", + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "404": { + "description": "Invalid user id", + "schema": {} + } + } + } + } + }, + "definitions": { + "dbc.Session": { + "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" + } + } + }, + "main.Jwt": { + "type": "object", + "properties": { + "token": { + "description": "The jwt token you can use for all authorized call to either keibi or other services.", + "type": "string" + } + } + }, + "main.LoginDto": { + "type": "object", + "required": [ + "login", + "password" + ], + "properties": { + "login": { + "description": "Either the email or the username.", + "type": "string" + }, + "password": { + "description": "Password of the account.", + "type": "string" + } + } + }, + "main.OidcHandle": { + "type": "object", + "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.", + "type": "string", + "format": "url" + }, + "username": { + "description": "Username of the user on the external service.", + "type": "string" + } + } + }, + "main.RegisterDto": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "description": "Valid email that could be used for forgotten password requests. Can be used for login.", + "type": "string", + "format": "email" + }, + "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" + } + } + }, + "main.Session": { + "type": "object", + "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" + } + } + }, + "main.User": { + "type": "object", + "properties": { + "claims": { + "description": "List of custom claims JWT created via get /jwt will have", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "createdDate": { + "description": "When was this account created?", + "type": "string" + }, + "email": { + "description": "Email of the user. Can be used as a login.", + "type": "string", + "format": "email" + }, + "id": { + "description": "Id of the user.", + "type": "string" + }, + "lastSeen": { + "description": "When was the last time this account made any authorized request?", + "type": "string" + }, + "oidc": { + "description": "List of other login method available for this user. Access tokens wont be returned here.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/main.OidcHandle" + } + }, + "username": { + "description": "Username of the user. Can be used as a login.", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "Jwt": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "Token": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "kyoo.zoriya.dev", + BasePath: "/auth", + Schemes: []string{}, + Title: "Keibi - Kyoo's auth", + Description: "Auth system made for kyoo.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/auth/docs/swagger.json b/auth/docs/swagger.json new file mode 100644 index 00000000..a41e8589 --- /dev/null +++ b/auth/docs/swagger.json @@ -0,0 +1,626 @@ +{ + "swagger": "2.0", + "info": { + "description": "Auth system made for kyoo.", + "title": "Keibi - Kyoo's auth", + "contact": { + "name": "Repository", + "url": "https://github.com/zoriya/kyoo" + }, + "license": { + "name": "GPL-3.0", + "url": "https://www.gnu.org/licenses/gpl-3.0.en.html" + }, + "version": "1.0" + }, + "host": "kyoo.zoriya.dev", + "basePath": "/auth", + "paths": { + "/info": { + "get": { + "description": "Get info like the public key used to sign the jwts.", + "produces": [ + "application/json" + ], + "tags": [ + "jwt" + ], + "summary": "Info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Info" + } + } + } + } + }, + "/jwt": { + "get": { + "security": [ + { + "Token": [] + } + ], + "description": "Convert a session token to a short lived JWT.", + "produces": [ + "application/json" + ], + "tags": [ + "jwt" + ], + "summary": "Get JWT", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.Jwt" + } + }, + "401": { + "description": "Missing session token", + "schema": {} + }, + "403": { + "description": "Invalid session token (or expired)", + "schema": {} + } + } + } + }, + "/sessions": { + "post": { + "description": "Login to your account and open a session", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "Login", + "parameters": [ + { + "type": "string", + "description": "The device the created session will be used on", + "name": "device", + "in": "query" + }, + { + "description": "Account informations", + "name": "login", + "in": "body", + "schema": { + "$ref": "#/definitions/main.LoginDto" + } + } + ], + "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": {} + } + } + } + }, + "/sessions/current": { + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete a session and logout", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "Logout", + "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": {} + } + } + } + }, + "/sessions/{id}": { + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete a session and logout", + "produces": [ + "application/json" + ], + "tags": [ + "sessions" + ], + "summary": "Logout", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "The id of the session to delete", + "name": "id", + "in": "path", + "required": true + } + ], + "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": {} + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "Jwt": [ + "users.read" + ] + } + ], + "description": "List all users existing in this instance.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List all users", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "used for pagination.", + "name": "afterId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "400": { + "description": "Invalid after id", + "schema": {} + } + } + }, + "post": { + "description": "Register as a new user and open a session for it", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Register", + "parameters": [ + { + "type": "string", + "description": "The device the created session will be used on", + "name": "device", + "in": "query" + }, + { + "description": "Registration informations", + "name": "user", + "in": "body", + "schema": { + "$ref": "#/definitions/main.RegisterDto" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dbc.Session" + } + }, + "400": { + "description": "Invalid register body", + "schema": {} + }, + "409": { + "description": "Duplicated email or username", + "schema": {} + } + } + } + }, + "/users/me": { + "get": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Get informations about the currently connected user", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get me", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "401": { + "description": "Missing jwt token", + "schema": {} + }, + "403": { + "description": "Invalid jwt token (or expired)", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "Jwt": [] + } + ], + "description": "Delete your account and all your sessions", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete self", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "Jwt": [ + "users.read" + ] + } + ], + "description": "Get informations about a user from it's id", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Get user", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "The id of the user", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "404": { + "description": "No user with the given id found", + "schema": {} + } + } + }, + "delete": { + "security": [ + { + "Jwt": [ + "users.delete" + ] + } + ], + "description": "Delete an account and all it's sessions.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete user", + "parameters": [ + { + "type": "string", + "format": "uuid", + "description": "User id of the user to delete", + "name": "id", + "in": "path" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/main.User" + } + }, + "404": { + "description": "Invalid user id", + "schema": {} + } + } + } + } + }, + "definitions": { + "dbc.Session": { + "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" + } + } + }, + "main.Jwt": { + "type": "object", + "properties": { + "token": { + "description": "The jwt token you can use for all authorized call to either keibi or other services.", + "type": "string" + } + } + }, + "main.LoginDto": { + "type": "object", + "required": [ + "login", + "password" + ], + "properties": { + "login": { + "description": "Either the email or the username.", + "type": "string" + }, + "password": { + "description": "Password of the account.", + "type": "string" + } + } + }, + "main.OidcHandle": { + "type": "object", + "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.", + "type": "string", + "format": "url" + }, + "username": { + "description": "Username of the user on the external service.", + "type": "string" + } + } + }, + "main.RegisterDto": { + "type": "object", + "required": [ + "email", + "password", + "username" + ], + "properties": { + "email": { + "description": "Valid email that could be used for forgotten password requests. Can be used for login.", + "type": "string", + "format": "email" + }, + "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" + } + } + }, + "main.Session": { + "type": "object", + "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" + } + } + }, + "main.User": { + "type": "object", + "properties": { + "claims": { + "description": "List of custom claims JWT created via get /jwt will have", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "createdDate": { + "description": "When was this account created?", + "type": "string" + }, + "email": { + "description": "Email of the user. Can be used as a login.", + "type": "string", + "format": "email" + }, + "id": { + "description": "Id of the user.", + "type": "string" + }, + "lastSeen": { + "description": "When was the last time this account made any authorized request?", + "type": "string" + }, + "oidc": { + "description": "List of other login method available for this user. Access tokens wont be returned here.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/main.OidcHandle" + } + }, + "username": { + "description": "Username of the user. Can be used as a login.", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "Jwt": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "Token": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/auth/docs/swagger.yaml b/auth/docs/swagger.yaml new file mode 100644 index 00000000..38ada431 --- /dev/null +++ b/auth/docs/swagger.yaml @@ -0,0 +1,426 @@ +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" diff --git a/auth/go.mod b/auth/go.mod index ac8dc635..3b15b790 100644 --- a/auth/go.mod +++ b/auth/go.mod @@ -1,31 +1,46 @@ module github.com/zoriya/kyoo/keibi -go 1.23 +go 1.23.3 + +toolchain go1.24.1 require ( github.com/alexedwards/argon2id v1.0.0 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 - github.com/jackc/pgx/v5 v5.7.2 + github.com/jackc/pgx/v5 v5.7.3 github.com/labstack/echo-jwt/v4 v4.2.0 github.com/labstack/echo/v4 v4.13.3 - github.com/otaxhu/problem v1.3.0 + 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/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 +) + require ( github.com/KyleBanks/depth v1.2.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/ghodss/yaml v1.0.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-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.23.0 - github.com/golang-migrate/migrate/v4 v4.18.1 + github.com/go-playground/validator/v10 v10.25.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 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 @@ -36,19 +51,19 @@ require ( github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/net v0.33.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.8.0 // indirect - golang.org/x/tools v0.28.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.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 + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/auth/go.sum b/auth/go.sum index 5a7baa85..f930ac64 100644 --- a/auth/go.sum +++ b/auth/go.sum @@ -9,8 +9,10 @@ github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6Ct github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= -github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +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/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= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= @@ -21,36 +23,38 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= -github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +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/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/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.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= -github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= +github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -64,8 +68,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= -github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +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/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= @@ -82,13 +86,25 @@ github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0 github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA= +github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -101,8 +117,10 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/otaxhu/problem v1.3.0 h1:w+eJZCbA+p1LBK4SmmX6mECMW1I2Du+yyPZ6QeEWiyI= -github.com/otaxhu/problem v1.3.0/go.mod h1:ih/V+2WmZaD5oSCJmhvQAdoKju1xfrgZTv6OiFlXjBY= +github.com/otaxhu/problem v1.4.0 h1:Pf4Hgn6bYwisSpW+gG/gVR8//yqPnOkLUJAZ2PX2Pzo= +github.com/otaxhu/problem v1.4.0/go.mod h1:KhJVvH7FFARjbo8zD4uhznhuB6OraYy3DT60IZaVdU0= +github.com/otaxhu/type-mismatch-encoding v0.0.0-20241118152201-1861af90dd01 h1:fu4zB0bmnJjRL/mAtA8dKCEw4VAYItmrM2Ry5xhqm+o= +github.com/otaxhu/type-mismatch-encoding v0.0.0-20241118152201-1861af90dd01/go.mod h1:I7A2jI8mxo2WCUaaDnzo+Bso12DxLs4lERe+R6M09/Y= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -111,7 +129,9 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk= @@ -138,36 +158,35 @@ go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +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/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= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -179,16 +198,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/auth/jwt.go b/auth/jwt.go index 91aacec4..21edaa63 100644 --- a/auth/jwt.go +++ b/auth/jwt.go @@ -2,8 +2,7 @@ package main import ( "context" - "crypto/x509" - "encoding/pem" + "fmt" "maps" "net/http" "strings" @@ -11,6 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/labstack/echo/v4" + "github.com/lestrrat-go/jwx/jwk" ) type Jwt struct { @@ -18,11 +18,6 @@ type Jwt struct { Token string `json:"token"` } -type Info struct { - // The public key used to sign jwt tokens. It can be used by your services to check if the jwt is valid. - PublicKey string `json:"publicKey"` -} - // @Summary Get JWT // @Description Convert a session token to a short lived JWT. // @Tags jwt @@ -53,40 +48,50 @@ func (h *Handler) CreateJwt(c echo.Context) error { }() claims := maps.Clone(session.User.Claims) + claims["username"] = session.User.Username claims["sub"] = session.User.Id.String() claims["sid"] = session.Id.String() - claims["iss"] = h.config.Issuer + claims["iss"] = h.config.PublicUrl + claims["iat"] = &jwt.NumericDate{ + Time: time.Now().UTC(), + } claims["exp"] = &jwt.NumericDate{ Time: time.Now().UTC().Add(time.Hour), } - claims["iss"] = &jwt.NumericDate{ - Time: time.Now().UTC(), - } jwt := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) t, err := jwt.SignedString(h.config.JwtPrivateKey) if err != nil { return err } + c.Response().Header().Add("Authorization", fmt.Sprintf("Bearer %s", t)) return c.JSON(http.StatusOK, Jwt{ Token: t, }) } -// @Summary Info -// @Description Get info like the public key used to sign the jwts. +// @Summary Jwks +// @Description Get the jwks info, used to validate jwts. // @Tags jwt // @Produce json -// @Success 200 {object} Info -// @Router /info [get] -func (h *Handler) GetInfo(c echo.Context) error { - key := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: x509.MarshalPKCS1PublicKey(h.config.JwtPublicKey), - }, - ) +// @Success 200 {object} jwk.Key +// @Router /.well-known/jwks.json [get] +func (h *Handler) GetJwks(c echo.Context) error { + key, err := jwk.New(h.config.JwtPublicKey) + if err != nil { + return err + } - return c.JSON(200, Info{ - PublicKey: string(key), + key.Set("use", "sig") + key.Set("key_ops", "verify") + set := jwk.NewSet() + set.Add(key) + return c.JSON(200, set) +} + +func (h *Handler) GetOidcConfig(c echo.Context) error { + return c.JSON(200, struct { + JwksUri string `json:"jwks_uri"` + }{ + JwksUri: fmt.Sprintf("%s/.well-known/jwks.json", h.config.PublicUrl), }) } diff --git a/auth/main.go b/auth/main.go index 14a1bea8..c6abeb09 100644 --- a/auth/main.go +++ b/auth/main.go @@ -43,7 +43,7 @@ type Validator struct { validator *validator.Validate } -func (v *Validator) Validate(i interface{}) error { +func (v *Validator) Validate(i any) error { if err := v.validator.Struct(i); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) } @@ -149,7 +149,7 @@ func main() { db, err := OpenDatabase() if err != nil { - e.Logger.Fatal("Could not open databse: ", err) + e.Logger.Fatal("Could not open database: ", err) return } @@ -184,7 +184,8 @@ func main() { r.DELETE("/sessions/:id", h.Logout) g.GET("/jwt", h.CreateJwt) - g.GET("/info", h.GetInfo) + e.GET("/.well-known/jwks.json", h.GetJwks) + e.GET("/.well-known/openid-configuration", h.GetOidcConfig) g.GET("/swagger/*", echoSwagger.WrapHandler) diff --git a/back/Dockerfile b/back/Dockerfile index b2c5e2b4..4bbc4f5b 100644 --- a/back/Dockerfile +++ b/back/Dockerfile @@ -25,5 +25,5 @@ COPY --from=builder /app /app WORKDIR /app EXPOSE 5000 # The back can take a long time to start if meilisearch is initializing -HEALTHCHECK --interval=5s --retries=15 CMD curl --fail http://localhost:5000/health || exit +HEALTHCHECK --interval=30s --retries=15 CMD curl --fail http://localhost:5000/health || exit ENTRYPOINT ["/app/kyoo"] diff --git a/back/Dockerfile.dev b/back/Dockerfile.dev index e33a87a5..b4428070 100644 --- a/back/Dockerfile.dev +++ b/back/Dockerfile.dev @@ -17,6 +17,6 @@ RUN dotnet restore WORKDIR /app EXPOSE 5000 ENV DOTNET_USE_POLLING_FILE_WATCHER 1 -# HEALTHCHECK --interval=5s CMD curl --fail http://localhost:5000/health || exit +# HEALTHCHECK --interval=30s CMD curl --fail http://localhost:5000/health || exit HEALTHCHECK CMD true ENTRYPOINT ["dotnet", "watch", "--non-interactive", "run", "--no-restore", "--project", "/app/src/Kyoo.Core"] diff --git a/docker-compose.dev-v5.yml b/docker-compose.dev-v5.yml new file mode 100644 index 00000000..ab734ca1 --- /dev/null +++ b/docker-compose.dev-v5.yml @@ -0,0 +1,190 @@ +x-transcoder: &transcoder-base + build: + context: ./transcoder + dockerfile: Dockerfile.dev + networks: + default: + aliases: + - transcoder + ports: + - "7666:7666" + restart: on-failure + cpus: 1 + env_file: + - ./.env + environment: + - GOCODER_PREFIX=/video + volumes: + - ./transcoder:/app + - ${LIBRARY_ROOT}:/video:ro + - ${CACHE_ROOT}:/cache + - transcoder_metadata:/metadata + +services: + front: + build: + context: ./front + dockerfile: Dockerfile.dev + volumes: + - ./front:/app + - /app/.yarn + - /app/node_modules + - /app/apps/mobile/node_modules + - /app/apps/web/.next/ + - /app/apps/mobile/.expo/ + ports: + - "3000:3000" + - "8081:8081" + restart: on-failure + environment: + - KYOO_URL=${KYOO_URL:-http://api:5000/api} + labels: + - "traefik.enable=true" + - "traefik.http.routers.front.rule=PathPrefix(`/`)" + + auth: + build: + context: ./auth + dockerfile: Dockerfile.dev + restart: on-failure + depends_on: + postgres: + condition: service_healthy + ports: + - "4568:4568" + env_file: + - ./.env + environment: + - KEIBI_PREFIX=/auth + volumes: + - ./auth:/app + labels: + - "traefik.enable=true" + - "traefik.http.routers.auth.rule=PathPrefix(`/auth/`)" + - "traefik.http.routers.auth.rule=PathPrefix(`/.well-known/`)" + + api: + build: + context: ./api + dockerfile: Dockerfile.dev + restart: on-failure + depends_on: + postgres: + condition: service_healthy + auth: + condition: service_healthy + volumes: + - ./api:/app + - /app/node_modules + ports: + - "3567:3567" + environment: + - KYOO_PREFIX=/api + env_file: + - ./.env + labels: + - "traefik.enable=true" + - "traefik.http.routers.api.rule=PathPrefix(`/api/`)" + - "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" + - "traefik.http.middlewares.phantom-token.forwardauth.authResponseHeaders=Authorization" + + # scanner: + # build: ./scanner + # restart: on-failure + # depends_on: + # back: + # condition: service_healthy + # env_file: + # - ./.env + # environment: + # - KYOO_URL=${KYOO_URL:-http://back:5000/api} + # volumes: + # - ${LIBRARY_ROOT}:/video:ro + # + # matcher: + # build: ./scanner + # command: matcher + # restart: on-failure + # depends_on: + # back: + # condition: service_healthy + # env_file: + # - ./.env + # environment: + # - KYOO_URL=${KYOO_URL:-http://back:5000/api} + + transcoder: + <<: *transcoder-base + profiles: ['', 'cpu'] + + transcoder-nvidia: + <<: *transcoder-base + deploy: + resources: + reservations: + devices: + - capabilities: [gpu] + driver: cdi + device_ids: + - nvidia.com/gpu=all + environment: + - GOCODER_PREFIX=/video + - GOCODER_HWACCEL=nvidia + profiles: ['nvidia'] + + transcoder-vaapi: + <<: *transcoder-base + devices: + - /dev/dri:/dev/dri + environment: + - GOCODER_PREFIX=/video + - GOCODER_HWACCEL=vaapi + - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} + profiles: ['vaapi'] + # qsv is the same setup as vaapi but with the hwaccel env var different + transcoder-qsv: + <<: *transcoder-base + devices: + - /dev/dri:/dev/dri + environment: + - GOCODER_PREFIX=/video + - GOCODER_HWACCEL=qsv + - GOCODER_VAAPI_RENDERER=${GOCODER_VAAPI_RENDERER:-/dev/dri/renderD128} + profiles: ['qsv'] + + traefik: + image: traefik:v3.3 + restart: on-failure + command: + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entryPoints.web.address=:8901" + - "--accesslog=true" + ports: + - "8901:8901" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" + + postgres: + image: postgres:15 + restart: on-failure + env_file: + - ./.env + volumes: + - db:/var/lib/postgresql/data + ports: + - "5432:5432" + environment: + - POSTGRES_HOST_AUTH_METHOD=trust + command: ["postgres", "-c", "log_statement=all"] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + db: + transcoder_metadata: diff --git a/transcoder/Dockerfile.dev b/transcoder/Dockerfile.dev index 16dd9d1d..f3c82da2 100644 --- a/transcoder/Dockerfile.dev +++ b/transcoder/Dockerfile.dev @@ -46,5 +46,8 @@ WORKDIR /app ENV NVIDIA_VISIBLE_DEVICES="all" ENV NVIDIA_DRIVER_CAPABILITIES="all" +COPY go.mod go.sum ./ +RUN go mod download + EXPOSE 7666 CMD ["wgo", "run", "-race", "."]