mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Add edit user/settings route + check for permissions (#873)
This commit is contained in:
commit
e9db7b6285
@ -93,3 +93,10 @@ RABBITMQ_HOST=rabbitmq
|
|||||||
RABBITMQ_PORT=5672
|
RABBITMQ_PORT=5672
|
||||||
RABBITMQ_DEFAULT_USER=kyoo
|
RABBITMQ_DEFAULT_USER=kyoo
|
||||||
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha
|
||||||
|
|
||||||
|
|
||||||
|
# v5 stuff, does absolutely nothing on master (aka: you can delete this)
|
||||||
|
EXTRA_CLAIMS='{"permissions": [], "verified": false}'
|
||||||
|
FIRST_USER_CLAIMS='{"permissions": ["user.read", "users.write", "users.delete"], "verified": true}'
|
||||||
|
GUEST_CLAIMS='{"permissions": []}'
|
||||||
|
PROTECTED_CLAIMS="permissions,verified"
|
||||||
|
1
.github/workflows/api-test.yml
vendored
1
.github/workflows/api-test.yml
vendored
@ -36,5 +36,4 @@ jobs:
|
|||||||
working-directory: ./api
|
working-directory: ./api
|
||||||
run: bun test
|
run: bun test
|
||||||
env:
|
env:
|
||||||
JWT_SECRET: "TODO"
|
|
||||||
POSTGRES_SERVER: localhost
|
POSTGRES_SERVER: localhost
|
||||||
|
5
.github/workflows/auth-hurl.yml
vendored
5
.github/workflows/auth-hurl.yml
vendored
@ -57,8 +57,3 @@ jobs:
|
|||||||
working-directory: ./auth
|
working-directory: ./auth
|
||||||
run: cat logs
|
run: cat logs
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: results
|
|
||||||
path: auth/out
|
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Elysia, { getSchemaValidator, t } from "elysia";
|
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||||
|
import Elysia, { t } from "elysia";
|
||||||
import { createRemoteJWKSet, jwtVerify } from "jose";
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
||||||
import { KError } from "./models/error";
|
|
||||||
|
|
||||||
const jwtSecret = process.env.JWT_SECRET
|
const jwtSecret = process.env.JWT_SECRET
|
||||||
? new TextEncoder().encode(process.env.JWT_SECRET)
|
? new TextEncoder().encode(process.env.JWT_SECRET)
|
||||||
@ -14,35 +14,47 @@ const jwks = createRemoteJWKSet(
|
|||||||
|
|
||||||
const Jwt = t.Object({
|
const Jwt = t.Object({
|
||||||
sub: t.String({ description: "User id" }),
|
sub: t.String({ description: "User id" }),
|
||||||
username: t.String(),
|
|
||||||
sid: t.String({ description: "Session id" }),
|
sid: t.String({ description: "Session id" }),
|
||||||
|
username: t.String(),
|
||||||
|
permissions: t.Array(t.String()),
|
||||||
});
|
});
|
||||||
const validator = getSchemaValidator(Jwt);
|
const validator = TypeCompiler.Compile(Jwt);
|
||||||
|
|
||||||
export const auth = new Elysia({ name: "auth" })
|
export const auth = new Elysia({ name: "auth" })
|
||||||
.guard({
|
.guard({
|
||||||
// Those are not applied for now. See https://github.com/elysiajs/elysia/issues/1139
|
headers: t.Object({
|
||||||
detail: {
|
authorization: t.TemplateLiteral("Bearer ${string}"),
|
||||||
security: [{ bearer: ["read"] }, { api: ["read"] }],
|
}),
|
||||||
},
|
|
||||||
response: {
|
|
||||||
401: { ...KError, description: "" },
|
|
||||||
403: { ...KError, description: "" },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.macro({
|
.macro({
|
||||||
permissions(perms: string[]) {
|
permissions(perms: string[]) {
|
||||||
return {
|
return {
|
||||||
resolve: async ({ headers: { authorization }, error }) => {
|
resolve: async ({ headers: { authorization }, error }) => {
|
||||||
console.log(process.env.JWT_ISSUER);
|
|
||||||
const bearer = authorization?.slice(7);
|
const bearer = authorization?.slice(7);
|
||||||
if (!bearer) return { jwt: false };
|
if (!bearer) {
|
||||||
|
return error(500, {
|
||||||
|
status: 500,
|
||||||
|
message: "No jwt, auth server configuration error.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-expect-error ts can't understand that there's two overload idk why
|
// @ts-expect-error ts can't understand that there's two overload idk why
|
||||||
const { payload } = await jwtVerify(bearer, jwtSecret ?? jwks, {
|
const { payload } = await jwtVerify(bearer, jwtSecret ?? jwks, {
|
||||||
issuer: process.env.JWT_ISSUER,
|
issuer: process.env.JWT_ISSUER,
|
||||||
});
|
});
|
||||||
// TODO: use perms
|
const jwt = validator.Decode(payload);
|
||||||
return { jwt: validator.Decode<typeof Jwt>(payload) };
|
|
||||||
|
for (const perm of perms) {
|
||||||
|
if (!jwt.permissions.includes(perm)) {
|
||||||
|
return error(403, {
|
||||||
|
status: 403,
|
||||||
|
message: `Missing permission: '${perm}'.`,
|
||||||
|
details: { current: jwt.permissions, required: perms },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { jwt };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import { auth } from "./auth";
|
||||||
import { entriesH } from "./controllers/entries";
|
import { entriesH } from "./controllers/entries";
|
||||||
import { imagesH } from "./controllers/images";
|
import { imagesH } from "./controllers/images";
|
||||||
import { seasonsH } from "./controllers/seasons";
|
import { seasonsH } from "./controllers/seasons";
|
||||||
@ -53,14 +54,43 @@ export const base = new Elysia({ name: "base" })
|
|||||||
export const prefix = process.env.KYOO_PREFIX ?? "";
|
export const prefix = process.env.KYOO_PREFIX ?? "";
|
||||||
export const app = new Elysia({ prefix })
|
export const app = new Elysia({ prefix })
|
||||||
.use(base)
|
.use(base)
|
||||||
.use(showsH)
|
.use(auth)
|
||||||
.use(movies)
|
.guard(
|
||||||
.use(series)
|
{
|
||||||
.use(collections)
|
// Those are not applied for now. See https://github.com/elysiajs/elysia/issues/1139
|
||||||
.use(entriesH)
|
detail: {
|
||||||
.use(seasonsH)
|
security: [{ bearer: ["core.read"] }, { api: ["core.read"] }],
|
||||||
.use(studiosH)
|
},
|
||||||
.use(staffH)
|
// See https://github.com/elysiajs/elysia/issues/1158
|
||||||
.use(videosH)
|
// response: {
|
||||||
.use(imagesH)
|
// 401: { ...KError, description: "" },
|
||||||
.use(seed);
|
// 403: { ...KError, description: "" },
|
||||||
|
// },
|
||||||
|
permissions: ["core.read"],
|
||||||
|
},
|
||||||
|
(app) =>
|
||||||
|
app
|
||||||
|
.use(showsH)
|
||||||
|
.use(movies)
|
||||||
|
.use(series)
|
||||||
|
.use(collections)
|
||||||
|
.use(entriesH)
|
||||||
|
.use(seasonsH)
|
||||||
|
.use(studiosH)
|
||||||
|
.use(staffH)
|
||||||
|
.use(imagesH),
|
||||||
|
)
|
||||||
|
.guard(
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
security: [{ bearer: ["core.write"] }, { api: ["core.write"] }],
|
||||||
|
},
|
||||||
|
// See https://github.com/elysiajs/elysia/issues/1158
|
||||||
|
// response: {
|
||||||
|
// 401: { ...KError, description: "" },
|
||||||
|
// 403: { ...KError, description: "" },
|
||||||
|
// },
|
||||||
|
permissions: ["core.write"],
|
||||||
|
},
|
||||||
|
(app) => app.use(videosH).use(seed),
|
||||||
|
);
|
||||||
|
@ -274,9 +274,12 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
|||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: desc.after })),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Entry),
|
200: Page(Entry),
|
||||||
404: {
|
404: {
|
||||||
|
@ -196,9 +196,12 @@ export const imagesH = new Elysia({ tags: ["images"] })
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.guard({
|
.guard({
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage(),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/studios/:id/logo",
|
"/studios/:id/logo",
|
||||||
|
@ -128,9 +128,12 @@ export const seasonsH = new Elysia({ tags: ["series"] })
|
|||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: desc.after })),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Season),
|
200: Page(Season),
|
||||||
404: {
|
404: {
|
||||||
|
@ -87,9 +87,12 @@ export const collections = new Elysia({
|
|||||||
description: "Include related resources in the response.",
|
description: "Include related resources in the response.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage(),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: { ...FullCollection, description: "Found" },
|
200: { ...FullCollection, description: "Found" },
|
||||||
404: {
|
404: {
|
||||||
@ -170,9 +173,12 @@ export const collections = new Elysia({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Collection),
|
200: Page(Collection),
|
||||||
422: KError,
|
422: KError,
|
||||||
@ -203,9 +209,12 @@ export const collections = new Elysia({
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/:id/movies",
|
"/:id/movies",
|
||||||
|
@ -77,9 +77,12 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
description: "Include related resources in the response.",
|
description: "Include related resources in the response.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage(),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: { ...FullMovie, description: "Found" },
|
200: { ...FullMovie, description: "Found" },
|
||||||
404: {
|
404: {
|
||||||
@ -160,9 +163,12 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Movie),
|
200: Page(Movie),
|
||||||
422: KError,
|
422: KError,
|
||||||
|
@ -77,9 +77,12 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
|||||||
description: "Include related resources in the response.",
|
description: "Include related resources in the response.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage(),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: { ...FullSerie, description: "Found" },
|
200: { ...FullSerie, description: "Found" },
|
||||||
404: {
|
404: {
|
||||||
@ -160,9 +163,12 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Serie),
|
200: Page(Serie),
|
||||||
422: KError,
|
422: KError,
|
||||||
|
@ -105,9 +105,12 @@ export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] })
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Show),
|
200: Page(Show),
|
||||||
422: KError,
|
422: KError,
|
||||||
|
@ -175,9 +175,12 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
description: "Include related resources in the response.",
|
description: "Include related resources in the response.",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage(),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: "studio",
|
200: "studio",
|
||||||
404: {
|
404: {
|
||||||
@ -249,9 +252,12 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: desc.after })),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Studio),
|
200: Page(Studio),
|
||||||
422: KError,
|
422: KError,
|
||||||
@ -282,9 +288,12 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
headers: t.Object({
|
headers: t.Object(
|
||||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
{
|
||||||
}),
|
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
})
|
})
|
||||||
.get(
|
.get(
|
||||||
"/:id/shows",
|
"/:id/shows",
|
||||||
|
17
api/tests/helpers/jwt.ts
Normal file
17
api/tests/helpers/jwt.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { SignJWT } from "jose";
|
||||||
|
|
||||||
|
export async function getJwtHeaders() {
|
||||||
|
const jwt = await new SignJWT({
|
||||||
|
sub: "39158be0-3f59-4c45-b00d-d25b3bc2b884",
|
||||||
|
sid: "04ac7ecc-255b-481d-b0c8-537c1578e3d5",
|
||||||
|
username: "test-username",
|
||||||
|
permissions: ["core.read", "core.write"],
|
||||||
|
})
|
||||||
|
.setProtectedHeader({ alg: "HS256" })
|
||||||
|
.setIssuedAt()
|
||||||
|
.setIssuer(process.env.JWT_ISSUER!)
|
||||||
|
.setExpirationTime("2h")
|
||||||
|
.sign(new TextEncoder().encode(process.env.JWT_SECRET));
|
||||||
|
|
||||||
|
return { Authorization: `Bearer ${jwt}` };
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
import type { SeedMovie } from "~/models/movie";
|
import type { SeedMovie } from "~/models/movie";
|
||||||
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const getMovie = async (
|
export const getMovie = async (
|
||||||
id: string,
|
id: string,
|
||||||
@ -15,8 +16,9 @@ export const getMovie = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -41,8 +43,9 @@ export const getMovies = async ({
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -56,6 +59,7 @@ export const createMovie = async (movie: SeedMovie) => {
|
|||||||
body: JSON.stringify(movie),
|
body: JSON.stringify(movie),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(await getJwtHeaders()),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
import type { SeedSerie } from "~/models/serie";
|
import type { SeedSerie } from "~/models/serie";
|
||||||
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const createSerie = async (serie: SeedSerie) => {
|
export const createSerie = async (serie: SeedSerie) => {
|
||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
@ -9,6 +10,7 @@ export const createSerie = async (serie: SeedSerie) => {
|
|||||||
body: JSON.stringify(serie),
|
body: JSON.stringify(serie),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(await getJwtHeaders()),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -29,8 +31,9 @@ export const getSerie = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -58,8 +61,9 @@ export const getSeasons = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -87,8 +91,9 @@ export const getEntries = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -108,6 +113,7 @@ export const getExtras = async (
|
|||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
new Request(buildUrl(`series/${serie}/extras`, opts), {
|
new Request(buildUrl(`series/${serie}/extras`, opts), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
headers: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -124,6 +130,7 @@ export const getUnknowns = async (opts: {
|
|||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
new Request(buildUrl("unknowns", opts), {
|
new Request(buildUrl("unknowns", opts), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
headers: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -147,8 +154,9 @@ export const getNews = async ({
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const getStaff = async (id: string, query: {}) => {
|
export const getStaff = async (id: string, query: {}) => {
|
||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
new Request(buildUrl(`staff/${id}`, query), {
|
new Request(buildUrl(`staff/${id}`, query), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
headers: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -32,8 +34,9 @@ export const getStaffRoles = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -52,6 +55,7 @@ export const getSerieStaff = async (
|
|||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
new Request(buildUrl(`series/${serie}/staff`, opts), {
|
new Request(buildUrl(`series/${serie}/staff`, opts), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
headers: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -70,6 +74,7 @@ export const getMovieStaff = async (
|
|||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
new Request(buildUrl(`movies/${movie}/staff`, opts), {
|
new Request(buildUrl(`movies/${movie}/staff`, opts), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
headers: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const getStudio = async (
|
export const getStudio = async (
|
||||||
id: string,
|
id: string,
|
||||||
@ -11,8 +12,9 @@ export const getStudio = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
@ -40,8 +42,9 @@ export const getShowsByStudio = async (
|
|||||||
headers: langs
|
headers: langs
|
||||||
? {
|
? {
|
||||||
"Accept-Language": langs,
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
}
|
}
|
||||||
: {},
|
: await getJwtHeaders(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { buildUrl } from "tests/utils";
|
import { buildUrl } from "tests/utils";
|
||||||
import { app } from "~/base";
|
import { app } from "~/base";
|
||||||
import type { SeedVideo } from "~/models/video";
|
import type { SeedVideo } from "~/models/video";
|
||||||
|
import { getJwtHeaders } from "./jwt";
|
||||||
|
|
||||||
export const createVideo = async (video: SeedVideo | SeedVideo[]) => {
|
export const createVideo = async (video: SeedVideo | SeedVideo[]) => {
|
||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
@ -9,6 +10,7 @@ export const createVideo = async (video: SeedVideo | SeedVideo[]) => {
|
|||||||
body: JSON.stringify(Array.isArray(video) ? video : [video]),
|
body: JSON.stringify(Array.isArray(video) ? video : [video]),
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
...(await getJwtHeaders()),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { beforeAll, describe, expect, it } from "bun:test";
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { getJwtHeaders } from "tests/helpers/jwt";
|
||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
@ -10,8 +11,8 @@ import { app, createMovie, getMovies } from "../helpers";
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await db.delete(shows);
|
await db.delete(shows);
|
||||||
for (const movie of [bubble, dune1984, dune]) {
|
for (const movie of [bubble, dune1984, dune]) {
|
||||||
const [ret, _] = await createMovie(movie);
|
const [ret, body] = await createMovie(movie);
|
||||||
expect(ret.status).toBe(201);
|
expectStatus(ret, body).toBe(201);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,7 +74,9 @@ describe("with a null value", () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
resp = await app.handle(new Request(next));
|
resp = await app.handle(
|
||||||
|
new Request(next, { headers: await getJwtHeaders() }),
|
||||||
|
);
|
||||||
body = await resp.json();
|
body = await resp.json();
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
@ -120,7 +123,9 @@ describe("with a null value", () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
resp = await app.handle(new Request(next));
|
resp = await app.handle(
|
||||||
|
new Request(next, { headers: await getJwtHeaders() }),
|
||||||
|
);
|
||||||
body = await resp.json();
|
body = await resp.json();
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { beforeAll, describe, expect, it } from "bun:test";
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { getJwtHeaders } from "tests/helpers/jwt";
|
||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
@ -71,7 +72,9 @@ describe("Get all movies", () => {
|
|||||||
});
|
});
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
|
|
||||||
resp = await app.handle(new Request(body.next));
|
resp = await app.handle(
|
||||||
|
new Request(body.next, { headers: await getJwtHeaders() }),
|
||||||
|
);
|
||||||
body = await resp.json();
|
body = await resp.json();
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
@ -104,7 +107,9 @@ describe("Get all movies", () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
resp = await app.handle(new Request(next));
|
resp = await app.handle(
|
||||||
|
new Request(next, { headers: await getJwtHeaders() }),
|
||||||
|
);
|
||||||
body = await resp.json();
|
body = await resp.json();
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
@ -160,7 +165,9 @@ describe("Get all movies", () => {
|
|||||||
expect(items.length).toBe(1);
|
expect(items.length).toBe(1);
|
||||||
expect(items[0].id).toBe(expectedIds[0]);
|
expect(items[0].id).toBe(expectedIds[0]);
|
||||||
// Get Second Page
|
// Get Second Page
|
||||||
resp = await app.handle(new Request(body.next));
|
resp = await app.handle(
|
||||||
|
new Request(body.next, { headers: await getJwtHeaders() }),
|
||||||
|
);
|
||||||
body = await resp.json();
|
body = await resp.json();
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
@ -175,7 +182,9 @@ describe("Get all movies", () => {
|
|||||||
});
|
});
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
|
|
||||||
const resp2 = await app.handle(new Request(body.next));
|
const resp2 = await app.handle(
|
||||||
|
new Request(body.next, { headers: await getJwtHeaders() }),
|
||||||
|
);
|
||||||
const body2 = await resp2.json();
|
const body2 = await resp2.json();
|
||||||
expectStatus(resp2, body).toBe(200);
|
expectStatus(resp2, body).toBe(200);
|
||||||
|
|
||||||
@ -187,7 +196,9 @@ describe("Get all movies", () => {
|
|||||||
|
|
||||||
it("Get /random", async () => {
|
it("Get /random", async () => {
|
||||||
const resp = await app.handle(
|
const resp = await app.handle(
|
||||||
new Request("http://localhost/movies/random"),
|
new Request("http://localhost/movies/random", {
|
||||||
|
headers: await getJwtHeaders(),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(resp.status).toBe(302);
|
expect(resp.status).toBe(302);
|
||||||
const location = resp.headers.get("location")!;
|
const location = resp.headers.get("location")!;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { beforeAll } from "bun:test";
|
import { beforeAll } from "bun:test";
|
||||||
import { migrate } from "~/db";
|
import { migrate } from "~/db";
|
||||||
|
|
||||||
|
process.env.JWT_SECRET = "this is a secret";
|
||||||
|
process.env.JWT_ISSUER = "https://kyoo.zoriya.dev";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await migrate();
|
await migrate();
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,11 @@ EXTRA_CLAIMS='{}'
|
|||||||
FIRST_USER_CLAIMS='{}'
|
FIRST_USER_CLAIMS='{}'
|
||||||
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
|
# If this is not empty, calls to `/jwt` without an `Authorization` header will still create a jwt (with `null` in `sub`)
|
||||||
GUEST_CLAIMS=""
|
GUEST_CLAIMS=""
|
||||||
|
# Comma separated list of claims that users without the `user.write` permissions should NOT be able to edit
|
||||||
|
# (if you don't specify this an user could make themself administrator for example)
|
||||||
|
# PS: `permissions` is always a protected claim since keibi uses it for user.read/user.write
|
||||||
|
PROTECTED_CLAIMS="permissions"
|
||||||
|
|
||||||
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
# The url you can use to reach your kyoo instance. This is used during oidc to redirect users to your instance.
|
||||||
PUBLIC_URL=http://localhost:8901
|
PUBLIC_URL=http://localhost:8901
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@ -22,12 +23,14 @@ type Configuration struct {
|
|||||||
DefaultClaims jwt.MapClaims
|
DefaultClaims jwt.MapClaims
|
||||||
FirstUserClaims jwt.MapClaims
|
FirstUserClaims jwt.MapClaims
|
||||||
GuestClaims jwt.MapClaims
|
GuestClaims jwt.MapClaims
|
||||||
|
ProtectedClaims []string
|
||||||
ExpirationDelay time.Duration
|
ExpirationDelay time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultConfig = Configuration{
|
var DefaultConfig = Configuration{
|
||||||
DefaultClaims: make(jwt.MapClaims),
|
DefaultClaims: make(jwt.MapClaims),
|
||||||
FirstUserClaims: make(jwt.MapClaims),
|
FirstUserClaims: make(jwt.MapClaims),
|
||||||
|
ProtectedClaims: []string{"permissions"},
|
||||||
ExpirationDelay: 30 * 24 * time.Hour,
|
ExpirationDelay: 30 * 24 * time.Hour,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,9 @@ func LoadConfiguration(db *dbc.Queries) (*Configuration, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected := strings.Split(os.Getenv("PROTECTED_CLAIMS"), ",")
|
||||||
|
ret.ProtectedClaims = append(ret.ProtectedClaims, protected...)
|
||||||
|
|
||||||
rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH")
|
rsa_pk_path := os.Getenv("RSA_PRIVATE_KEY_PATH")
|
||||||
if rsa_pk_path != "" {
|
if rsa_pk_path != "" {
|
||||||
privateKeyData, err := os.ReadFile(rsa_pk_path)
|
privateKeyData, err := os.ReadFile(rsa_pk_path)
|
||||||
|
@ -12,6 +12,23 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const clearOtherSessions = `-- name: ClearOtherSessions :exec
|
||||||
|
delete from sessions as s using users as u
|
||||||
|
where s.user_pk = u.pk
|
||||||
|
and s.id != $1
|
||||||
|
and u.id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type ClearOtherSessionsParams struct {
|
||||||
|
SessionId uuid.UUID `json:"sessionId"`
|
||||||
|
UserId uuid.UUID `json:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ClearOtherSessions(ctx context.Context, arg ClearOtherSessionsParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, clearOtherSessions, arg.SessionId, arg.UserId)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const createSession = `-- name: CreateSession :one
|
const createSession = `-- name: CreateSession :one
|
||||||
insert into sessions(token, user_pk, device)
|
insert into sessions(token, user_pk, device)
|
||||||
values ($1, $2, $3)
|
values ($1, $2, $3)
|
||||||
|
@ -265,10 +265,10 @@ const updateUser = `-- name: UpdateUser :one
|
|||||||
update
|
update
|
||||||
users
|
users
|
||||||
set
|
set
|
||||||
username = $2,
|
username = coalesce($2, username),
|
||||||
email = $3,
|
email = coalesce($3, email),
|
||||||
password = $4,
|
password = coalesce($4, password),
|
||||||
claims = $5
|
claims = coalesce($5, claims)
|
||||||
where
|
where
|
||||||
id = $1
|
id = $1
|
||||||
returning
|
returning
|
||||||
@ -277,8 +277,8 @@ returning
|
|||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateUserParams struct {
|
||||||
Id uuid.UUID `json:"id"`
|
Id uuid.UUID `json:"id"`
|
||||||
Username string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Email string `json:"email"`
|
Email *string `json:"email"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
Claims jwt.MapClaims `json:"claims"`
|
Claims jwt.MapClaims `json:"claims"`
|
||||||
}
|
}
|
||||||
|
@ -377,6 +377,102 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Jwt": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Edit your account's info",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Edit self",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Edited user info",
|
||||||
|
"name": "user",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.EditUserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "You can't edit a protected claim",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/me/password": {
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Jwt": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Edit your password",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Edit password",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Invalidate other sessions",
|
||||||
|
"name": "invalidate",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "New password",
|
||||||
|
"name": "user",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.EditPasswordDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/users/{id}": {
|
"/users/{id}": {
|
||||||
@ -469,10 +565,101 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Jwt": [
|
||||||
|
"users.write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Edit an account info or permissions",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Edit user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "User id of the user to edit",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Edited user info",
|
||||||
|
"name": "user",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.EditUserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "You don't have permissions to edit another account",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"main.EditPasswordDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "password1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main.EditUserDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"claims": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"preferOriginal": " true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "kyoo@zoriya.dev"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "zoriya"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.JwkSet": {
|
"main.JwkSet": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -371,6 +371,102 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Jwt": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Edit your account's info",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Edit self",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Edited user info",
|
||||||
|
"name": "user",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.EditUserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "You can't edit a protected claim",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/users/me/password": {
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Jwt": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Edit your password",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Edit password",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Invalidate other sessions",
|
||||||
|
"name": "invalidate",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "New password",
|
||||||
|
"name": "user",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.EditPasswordDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/users/{id}": {
|
"/users/{id}": {
|
||||||
@ -463,10 +559,101 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Jwt": [
|
||||||
|
"users.write"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Edit an account info or permissions",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"users"
|
||||||
|
],
|
||||||
|
"summary": "Edit user",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"description": "User id of the user to edit",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Edited user info",
|
||||||
|
"name": "user",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.EditUserDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.User"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "You don't have permissions to edit another account",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Invalid body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/main.KError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
|
"main.EditPasswordDto": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"password": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "password1234"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main.EditUserDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"claims": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"preferOriginal": " true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "kyoo@zoriya.dev"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "zoriya"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"main.JwkSet": {
|
"main.JwkSet": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -53,7 +53,7 @@ type Validator struct {
|
|||||||
|
|
||||||
func (v *Validator) Validate(i any) error {
|
func (v *Validator) Validate(i any) error {
|
||||||
if err := v.validator.Struct(i); err != nil {
|
if err := v.validator.Struct(i); err != nil {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -215,6 +215,9 @@ func main() {
|
|||||||
r.GET("/users/me", h.GetMe)
|
r.GET("/users/me", h.GetMe)
|
||||||
r.DELETE("/users/:id", h.DeleteUser)
|
r.DELETE("/users/:id", h.DeleteUser)
|
||||||
r.DELETE("/users/me", h.DeleteSelf)
|
r.DELETE("/users/me", h.DeleteSelf)
|
||||||
|
r.PATCH("/users/:id", h.EditUser)
|
||||||
|
r.PATCH("/users/me", h.EditSelf)
|
||||||
|
r.PATCH("/users/me/password", h.ChangePassword)
|
||||||
g.POST("/users", h.Register)
|
g.POST("/users", h.Register)
|
||||||
|
|
||||||
g.POST("/sessions", h.Login)
|
g.POST("/sessions", h.Login)
|
||||||
|
@ -125,7 +125,7 @@ func (h *Handler) createSession(c echo.Context, user *User) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.JSON(201, session)
|
return c.JSON(201, MapSessionToken(&session))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Logout
|
// @Summary Logout
|
||||||
|
@ -43,3 +43,8 @@ where s.user_pk = u.pk
|
|||||||
returning
|
returning
|
||||||
s.*;
|
s.*;
|
||||||
|
|
||||||
|
-- name: ClearOtherSessions :exec
|
||||||
|
delete from sessions as s using users as u
|
||||||
|
where s.user_pk = u.pk
|
||||||
|
and s.id != @session_id
|
||||||
|
and u.id = @user_id;
|
||||||
|
@ -67,10 +67,10 @@ returning
|
|||||||
update
|
update
|
||||||
users
|
users
|
||||||
set
|
set
|
||||||
username = $2,
|
username = coalesce(sqlc.narg(username), username),
|
||||||
email = $3,
|
email = coalesce(sqlc.narg(email), email),
|
||||||
password = $4,
|
password = coalesce(sqlc.narg(password), password),
|
||||||
claims = $5
|
claims = coalesce(sqlc.narg(claims), claims)
|
||||||
where
|
where
|
||||||
id = $1
|
id = $1
|
||||||
returning
|
returning
|
||||||
|
40
auth/tests/change-password.hurl
Normal file
40
auth/tests/change-password.hurl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
POST {{host}}/users
|
||||||
|
{
|
||||||
|
"username": "edit-password",
|
||||||
|
"password": "password-login-user",
|
||||||
|
"email": "invalid-password-user@zoriya.dev"
|
||||||
|
}
|
||||||
|
HTTP 201
|
||||||
|
[Captures]
|
||||||
|
first_token: jsonpath "$.token"
|
||||||
|
|
||||||
|
POST {{host}}/sessions
|
||||||
|
{
|
||||||
|
"login": "edit-password",
|
||||||
|
"password": "password-login-user"
|
||||||
|
}
|
||||||
|
HTTP 201
|
||||||
|
[Captures]
|
||||||
|
token: jsonpath "$.token"
|
||||||
|
|
||||||
|
GET {{host}}/jwt
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
HTTP 200
|
||||||
|
[Captures]
|
||||||
|
jwt: jsonpath "$.token"
|
||||||
|
|
||||||
|
PATCH {{host}}/users/me/password
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
{
|
||||||
|
"password": "new-password"
|
||||||
|
}
|
||||||
|
HTTP 204
|
||||||
|
|
||||||
|
# Invalid password login
|
||||||
|
POST {{host}}/jwt
|
||||||
|
Authorization: Bearer {{first_token}}
|
||||||
|
HTTP 403
|
||||||
|
|
||||||
|
DELETE {{host}}/users/me
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
HTTP 200
|
35
auth/tests/edit-settings.hurl
Normal file
35
auth/tests/edit-settings.hurl
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
POST {{host}}/users
|
||||||
|
{
|
||||||
|
"username": "edit-settings",
|
||||||
|
"password": "password-login-user",
|
||||||
|
"email": "edit-settings@zoriya.dev"
|
||||||
|
}
|
||||||
|
HTTP 201
|
||||||
|
[Captures]
|
||||||
|
token: jsonpath "$.token"
|
||||||
|
|
||||||
|
GET {{host}}/jwt
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
HTTP 200
|
||||||
|
[Captures]
|
||||||
|
jwt: jsonpath "$.token"
|
||||||
|
|
||||||
|
PATCH {{host}}/users/me
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
{
|
||||||
|
"claims": {
|
||||||
|
"preferOriginal": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HTTP 200
|
||||||
|
[Asserts]
|
||||||
|
jsonpath "$.claims.preferOriginal" == true
|
||||||
|
jsonpath "$.username" == "edit-settings"
|
||||||
|
|
||||||
|
GET {{host}}/jwt
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
HTTP 200
|
||||||
|
|
||||||
|
DELETE {{host}}/users/me
|
||||||
|
Authorization: Bearer {{jwt}}
|
||||||
|
HTTP 200
|
162
auth/users.go
162
auth/users.go
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -51,6 +52,16 @@ type RegisterDto struct {
|
|||||||
Password string `json:"password" validate:"required" example:"password1234"`
|
Password string `json:"password" validate:"required" example:"password1234"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EditUserDto struct {
|
||||||
|
Username *string `json:"username,omitempty" validate:"omitnil,excludes=@" example:"zoriya"`
|
||||||
|
Email *string `json:"email,omitempty" validate:"omitnil,email" example:"kyoo@zoriya.dev"`
|
||||||
|
Claims jwt.MapClaims `json:"claims,omitempty" example:"preferOriginal: true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditPasswordDto struct {
|
||||||
|
Password string `json:"password" validate:"required" example:"password1234"`
|
||||||
|
}
|
||||||
|
|
||||||
func MapDbUser(user *dbc.User) User {
|
func MapDbUser(user *dbc.User) User {
|
||||||
return User{
|
return User{
|
||||||
Pk: user.Pk,
|
Pk: user.Pk,
|
||||||
@ -235,6 +246,11 @@ func (h *Handler) Register(c echo.Context) error {
|
|||||||
// @Failure 404 {object} KError "Invalid user id"
|
// @Failure 404 {object} KError "Invalid user id"
|
||||||
// @Router /users/{id} [delete]
|
// @Router /users/{id} [delete]
|
||||||
func (h *Handler) DeleteUser(c echo.Context) error {
|
func (h *Handler) DeleteUser(c echo.Context) error {
|
||||||
|
err := CheckPermissions(c, []string{"user.delete"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
uid, err := uuid.Parse(c.Param("id"))
|
uid, err := uuid.Parse(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
|
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
|
||||||
@ -271,3 +287,149 @@ func (h *Handler) DeleteSelf(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
return c.JSON(200, MapDbUser(&ret))
|
return c.JSON(200, MapDbUser(&ret))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Summary Edit self
|
||||||
|
// @Description Edit your account's info
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt
|
||||||
|
// @Param user body EditUserDto false "Edited user info"
|
||||||
|
// @Success 200 {object} User
|
||||||
|
// @Success 403 {object} KError "You can't edit a protected claim"
|
||||||
|
// @Success 422 {object} KError "Invalid body"
|
||||||
|
// @Router /users/me [patch]
|
||||||
|
func (h *Handler) EditSelf(c echo.Context) error {
|
||||||
|
var req EditUserDto
|
||||||
|
err := c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||||
|
}
|
||||||
|
if err = c.Validate(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range h.config.ProtectedClaims {
|
||||||
|
if _, contains := req.Claims[key]; contains {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("Can't edit protected claim: '%s'.", key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := GetCurrentUserId(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := h.db.UpdateUser(context.Background(), dbc.UpdateUserParams{
|
||||||
|
Id: uid,
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
Claims: req.Claims,
|
||||||
|
})
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Invalid token, user not found.")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, MapDbUser(&ret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Edit user
|
||||||
|
// @Description Edit an account info or permissions
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt[users.write]
|
||||||
|
// @Param id path string false "User id of the user to edit" Format(uuid)
|
||||||
|
// @Param user body EditUserDto false "Edited user info"
|
||||||
|
// @Success 200 {object} User
|
||||||
|
// @Success 403 {object} KError "You don't have permissions to edit another account"
|
||||||
|
// @Success 422 {object} KError "Invalid body"
|
||||||
|
// @Router /users/{id} [patch]
|
||||||
|
func (h *Handler) EditUser(c echo.Context) error {
|
||||||
|
err := CheckPermissions(c, []string{"user.write"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(400, "Invalid id given: not an uuid")
|
||||||
|
}
|
||||||
|
|
||||||
|
var req EditUserDto
|
||||||
|
err = c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||||
|
}
|
||||||
|
if err = c.Validate(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := h.db.UpdateUser(context.Background(), dbc.UpdateUserParams{
|
||||||
|
Id: uid,
|
||||||
|
Username: req.Username,
|
||||||
|
Email: req.Email,
|
||||||
|
Claims: req.Claims,
|
||||||
|
})
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Invalid user id, user not found")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(200, MapDbUser(&ret))
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Edit password
|
||||||
|
// @Description Edit your password
|
||||||
|
// @Tags users
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Security Jwt
|
||||||
|
// @Param invalidate query bool false "Invalidate other sessions" default(true)
|
||||||
|
// @Param user body EditPasswordDto false "New password"
|
||||||
|
// @Success 204
|
||||||
|
// @Success 422 {object} KError "Invalid body"
|
||||||
|
// @Router /users/me/password [patch]
|
||||||
|
func (h *Handler) ChangePassword(c echo.Context) error {
|
||||||
|
uid, err := GetCurrentUserId(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sid, err := GetCurrentSessionId(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req EditPasswordDto
|
||||||
|
err = c.Bind(&req)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
|
||||||
|
}
|
||||||
|
if err = c.Validate(&req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = h.db.UpdateUser(context.Background(), dbc.UpdateUserParams{
|
||||||
|
Id: uid,
|
||||||
|
Password: &req.Password,
|
||||||
|
})
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "Invalid token, user not found")
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.db.ClearOtherSessions(context.Background(), dbc.ClearOtherSessionsParams{
|
||||||
|
SessionId: sid,
|
||||||
|
UserId: uid,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.NoContent(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
@ -28,6 +28,32 @@ func GetCurrentUserId(c echo.Context) (uuid.UUID, error) {
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetCurrentSessionId(c echo.Context) (uuid.UUID, error) {
|
||||||
|
user := c.Get("user").(*jwt.Token)
|
||||||
|
if user == nil {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(401, "Unauthorized")
|
||||||
|
}
|
||||||
|
claims, ok := user.Claims.(jwt.MapClaims)
|
||||||
|
if !ok {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(403, "Could not retrieve claims")
|
||||||
|
}
|
||||||
|
sid, ok := claims["sid"]
|
||||||
|
if !ok {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(403, "Could not retrieve session")
|
||||||
|
}
|
||||||
|
|
||||||
|
sid_str, ok := sid.(string)
|
||||||
|
if !ok {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(403, "Invalid session id claim.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ret, err := uuid.Parse(sid_str)
|
||||||
|
if err != nil {
|
||||||
|
return uuid.UUID{}, echo.NewHTTPError(403, "Invalid id")
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func CheckPermissions(c echo.Context, perms []string) error {
|
func CheckPermissions(c echo.Context, perms []string) error {
|
||||||
token, ok := c.Get("user").(*jwt.Token)
|
token, ok := c.Get("user").(*jwt.Token)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user