mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05: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,6 +54,22 @@ 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(auth)
 | 
				
			||||||
 | 
						.guard(
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								// Those are not applied for now. See https://github.com/elysiajs/elysia/issues/1139
 | 
				
			||||||
 | 
								detail: {
 | 
				
			||||||
 | 
									security: [{ bearer: ["core.read"] }, { api: ["core.read"] }],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								// See https://github.com/elysiajs/elysia/issues/1158
 | 
				
			||||||
 | 
								// response: {
 | 
				
			||||||
 | 
								// 	401: { ...KError, description: "" },
 | 
				
			||||||
 | 
								// 	403: { ...KError, description: "" },
 | 
				
			||||||
 | 
								// },
 | 
				
			||||||
 | 
								permissions: ["core.read"],
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							(app) =>
 | 
				
			||||||
 | 
								app
 | 
				
			||||||
				.use(showsH)
 | 
									.use(showsH)
 | 
				
			||||||
				.use(movies)
 | 
									.use(movies)
 | 
				
			||||||
				.use(series)
 | 
									.use(series)
 | 
				
			||||||
@ -61,6 +78,19 @@ export const app = new Elysia({ prefix })
 | 
				
			|||||||
				.use(seasonsH)
 | 
									.use(seasonsH)
 | 
				
			||||||
				.use(studiosH)
 | 
									.use(studiosH)
 | 
				
			||||||
				.use(staffH)
 | 
									.use(staffH)
 | 
				
			||||||
	.use(videosH)
 | 
									.use(imagesH),
 | 
				
			||||||
	.use(imagesH)
 | 
						)
 | 
				
			||||||
	.use(seed);
 | 
						.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