Integerate the auth service to the api (#857)

This commit is contained in:
Zoe Roux 2025-03-25 11:22:12 +01:00 committed by GitHub
commit 6d09c3354c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 2749 additions and 145 deletions

View File

@ -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=

View File

@ -24,6 +24,5 @@ WORKDIR /app
COPY --from=builder /app/server server
ENV NODE_ENV=production
EXPOSE 3000
EXPOSE 3567
CMD ["./server"]

10
api/Dockerfile.dev Normal file
View File

@ -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"]

View File

@ -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=="],

View File

@ -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"

47
api/src/auth.ts Normal file
View File

@ -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<typeof Jwt>(payload) };
},
};
},
})
.as("plugin");

View File

@ -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)

View File

@ -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." },

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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: {

View File

@ -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}`);

View File

@ -4,4 +4,4 @@ export * from "./studio-helper";
export * from "./staff-helper";
export * from "./videos-helper";
export * from "~/elysia";
export * from "~/base";

View File

@ -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 (

View File

@ -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) => {

View File

@ -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(

View File

@ -1,5 +1,5 @@
import { buildUrl } from "tests/utils";
import { app } from "~/elysia";
import { app } from "~/base";
export const getStudio = async (
id: string,

View File

@ -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[]) => {

View File

@ -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

4
auth/.gitignore vendored
View File

@ -1,4 +0,0 @@
# generated via sqlc
dbc/
# genereated via swag
docs/

View File

@ -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"]

View File

@ -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", "."]

View File

@ -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 {

73
auth/dbc/config.sql.go Normal file
View File

@ -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
}

32
auth/dbc/db.go Normal file
View File

@ -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,
}
}

49
auth/dbc/models.go Normal file
View File

@ -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"`
}

161
auth/dbc/sessions.sql.go Normal file
View File

@ -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
}

296
auth/dbc/users.sql.go Normal file
View File

@ -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
}

650
auth/docs/docs.go Normal file
View File

@ -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)
}

626
auth/docs/swagger.json Normal file
View File

@ -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"
}
}
}

426
auth/docs/swagger.yaml Normal file
View File

@ -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"

View File

@ -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
)

View File

@ -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=

View File

@ -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),
})
}

View File

@ -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)

View File

@ -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"]

View File

@ -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"]

190
docker-compose.dev-v5.yml Normal file
View File

@ -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:

View File

@ -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", "."]