mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 00:30:02 +00:00
Enable avatar upload for users
This commit is contained in:
@@ -2,11 +2,9 @@
|
|||||||
<configuration default="false" name="GameyfinApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
<configuration default="false" name="GameyfinApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||||
<option name="ACTIVE_PROFILES" value="dev" />
|
<option name="ACTIVE_PROFILES" value="dev" />
|
||||||
<option name="ALTERNATIVE_JRE_PATH" value="BUNDLED" />
|
<option name="ALTERNATIVE_JRE_PATH" value="BUNDLED" />
|
||||||
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="true" />
|
|
||||||
<module name="gameyfin.main" />
|
<module name="gameyfin.main" />
|
||||||
<option name="SHORTEN_COMMAND_LINE" value="ARGS_FILE" />
|
<option name="SHORTEN_COMMAND_LINE" value="ARGS_FILE" />
|
||||||
<option name="SPRING_BOOT_MAIN_CLASS" value="de.grimsi.gameyfin.GameyfinApplication" />
|
<option name="SPRING_BOOT_MAIN_CLASS" value="de.grimsi.gameyfin.GameyfinApplication" />
|
||||||
<option name="VM_PARAMETERS" value="-XX:+AllowEnhancedClassRedefinition -XX:HotswapAgent=fatjar" />
|
|
||||||
<extension name="coverage">
|
<extension name="coverage">
|
||||||
<pattern>
|
<pattern>
|
||||||
<option name="PATTERN" value="de.grimsi.gameyfin.*" />
|
<option name="PATTERN" value="de.grimsi.gameyfin.*" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080" useFirstLineBreakpoints="true">
|
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080" useFirstLineBreakpoints="true">
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
+1
-1
@@ -53,7 +53,7 @@ dependencies {
|
|||||||
|
|
||||||
// Persistence
|
// Persistence
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.7")
|
implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.14")
|
||||||
implementation("org.springframework.cloud:spring-cloud-starter")
|
implementation("org.springframework.cloud:spring-cloud-starter")
|
||||||
|
|
||||||
// Development
|
// Development
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
kotlinVersion=2.0.20
|
kotlinVersion=2.0.20
|
||||||
vaadinVersion=24.4.10
|
vaadinVersion=24.4.11
|
||||||
springBootVersion=3.3.3
|
springBootVersion=3.3.3
|
||||||
springCloudVersion=2023.0.3
|
springCloudVersion=2023.0.3
|
||||||
springDependencyManagementVersion=1.1.6
|
springDependencyManagementVersion=1.1.6
|
||||||
Generated
+1591
-2240
File diff suppressed because it is too large
Load Diff
+59
-59
@@ -7,24 +7,24 @@
|
|||||||
"@nextui-org/react": "^2.4.6",
|
"@nextui-org/react": "^2.4.6",
|
||||||
"@phosphor-icons/react": "^2.1.7",
|
"@phosphor-icons/react": "^2.1.7",
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@vaadin/bundles": "24.4.5",
|
"@vaadin/bundles": "24.4.7",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.4.6",
|
"@vaadin/hilla-file-router": "24.4.7",
|
||||||
"@vaadin/hilla-frontend": "24.4.6",
|
"@vaadin/hilla-frontend": "24.4.7",
|
||||||
"@vaadin/hilla-lit-form": "24.4.6",
|
"@vaadin/hilla-lit-form": "24.4.7",
|
||||||
"@vaadin/hilla-react-auth": "24.4.6",
|
"@vaadin/hilla-react-auth": "24.4.7",
|
||||||
"@vaadin/hilla-react-crud": "24.4.6",
|
"@vaadin/hilla-react-crud": "24.4.7",
|
||||||
"@vaadin/hilla-react-form": "24.4.6",
|
"@vaadin/hilla-react-form": "24.4.7",
|
||||||
"@vaadin/hilla-react-i18n": "24.4.6",
|
"@vaadin/hilla-react-i18n": "24.4.7",
|
||||||
"@vaadin/hilla-react-signals": "24.4.6",
|
"@vaadin/hilla-react-signals": "24.4.7",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.4.5",
|
"@vaadin/polymer-legacy-adapter": "24.4.7",
|
||||||
"@vaadin/react-components": "24.4.5",
|
"@vaadin/react-components": "24.4.7",
|
||||||
"@vaadin/router": "1.7.5",
|
"@vaadin/router": "1.7.5",
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.4.5",
|
"@vaadin/vaadin-lumo-styles": "24.4.7",
|
||||||
"@vaadin/vaadin-material-styles": "24.4.5",
|
"@vaadin/vaadin-material-styles": "24.4.7",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.4.5",
|
"@vaadin/vaadin-themable-mixin": "24.4.7",
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.2",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router-dom": "6.23.1",
|
"react-router-dom": "6.26.1",
|
||||||
"sonner": "^1.5.0",
|
"sonner": "^1.5.0",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"yup": "^1.4.0"
|
"yup": "^1.4.0"
|
||||||
@@ -49,22 +49,22 @@
|
|||||||
"@rollup/plugin-replace": "5.0.7",
|
"@rollup/plugin-replace": "5.0.7",
|
||||||
"@rollup/pluginutils": "5.1.0",
|
"@rollup/pluginutils": "5.1.0",
|
||||||
"@types/node": "^22.4.0",
|
"@types/node": "^22.4.0",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.4",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@vaadin/hilla-generator-cli": "24.4.6",
|
"@vaadin/hilla-generator-cli": "24.4.7",
|
||||||
"@vaadin/hilla-generator-core": "24.4.6",
|
"@vaadin/hilla-generator-core": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.4.6",
|
"@vaadin/hilla-generator-plugin-backbone": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.4.6",
|
"@vaadin/hilla-generator-plugin-barrel": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.4.6",
|
"@vaadin/hilla-generator-plugin-client": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.4.6",
|
"@vaadin/hilla-generator-plugin-model": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.4.6",
|
"@vaadin/hilla-generator-plugin-push": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.4.6",
|
"@vaadin/hilla-generator-plugin-subtypes": "24.4.7",
|
||||||
"@vaadin/hilla-generator-utils": "24.4.6",
|
"@vaadin/hilla-generator-utils": "24.4.7",
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
"@vitejs/plugin-react": "4.3.1",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||||
"async": "3.2.5",
|
"async": "3.2.6",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"glob": "10.4.1",
|
"glob": "10.4.5",
|
||||||
"postcss": "^8.4.41",
|
"postcss": "^8.4.41",
|
||||||
"postcss-import": "^16.1.0",
|
"postcss-import": "^16.1.0",
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"tailwindcss": "^3.4.10",
|
"tailwindcss": "^3.4.10",
|
||||||
"transform-ast": "2.4.4",
|
"transform-ast": "2.4.4",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.4.5",
|
||||||
"vite": "5.3.3",
|
"vite": "5.4.2",
|
||||||
"vite-plugin-checker": "0.6.4",
|
"vite-plugin-checker": "0.6.4",
|
||||||
"workbox-build": "7.1.1",
|
"workbox-build": "7.1.1",
|
||||||
"workbox-core": "7.1.0",
|
"workbox-core": "7.1.0",
|
||||||
@@ -123,60 +123,60 @@
|
|||||||
"vaadin": {
|
"vaadin": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@polymer/polymer": "3.5.1",
|
"@polymer/polymer": "3.5.1",
|
||||||
"@vaadin/bundles": "24.4.5",
|
"@vaadin/bundles": "24.4.7",
|
||||||
"@vaadin/common-frontend": "0.0.19",
|
"@vaadin/common-frontend": "0.0.19",
|
||||||
"@vaadin/hilla-file-router": "24.4.6",
|
"@vaadin/hilla-file-router": "24.4.7",
|
||||||
"@vaadin/hilla-frontend": "24.4.6",
|
"@vaadin/hilla-frontend": "24.4.7",
|
||||||
"@vaadin/hilla-lit-form": "24.4.6",
|
"@vaadin/hilla-lit-form": "24.4.7",
|
||||||
"@vaadin/hilla-react-auth": "24.4.6",
|
"@vaadin/hilla-react-auth": "24.4.7",
|
||||||
"@vaadin/hilla-react-crud": "24.4.6",
|
"@vaadin/hilla-react-crud": "24.4.7",
|
||||||
"@vaadin/hilla-react-form": "24.4.6",
|
"@vaadin/hilla-react-form": "24.4.7",
|
||||||
"@vaadin/hilla-react-i18n": "24.4.6",
|
"@vaadin/hilla-react-i18n": "24.4.7",
|
||||||
"@vaadin/hilla-react-signals": "24.4.6",
|
"@vaadin/hilla-react-signals": "24.4.7",
|
||||||
"@vaadin/polymer-legacy-adapter": "24.4.5",
|
"@vaadin/polymer-legacy-adapter": "24.4.7",
|
||||||
"@vaadin/react-components": "24.4.5",
|
"@vaadin/react-components": "24.4.7",
|
||||||
"@vaadin/router": "1.7.5",
|
"@vaadin/router": "1.7.5",
|
||||||
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
"@vaadin/vaadin-development-mode-detector": "2.0.7",
|
||||||
"@vaadin/vaadin-lumo-styles": "24.4.5",
|
"@vaadin/vaadin-lumo-styles": "24.4.7",
|
||||||
"@vaadin/vaadin-material-styles": "24.4.5",
|
"@vaadin/vaadin-material-styles": "24.4.7",
|
||||||
"@vaadin/vaadin-themable-mixin": "24.4.5",
|
"@vaadin/vaadin-themable-mixin": "24.4.7",
|
||||||
"@vaadin/vaadin-usage-statistics": "2.1.2",
|
"@vaadin/vaadin-usage-statistics": "2.1.3",
|
||||||
"construct-style-sheets-polyfill": "3.1.0",
|
"construct-style-sheets-polyfill": "3.1.0",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"lit": "3.1.4",
|
"lit": "3.1.4",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-router-dom": "6.23.1"
|
"react-router-dom": "6.26.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "7.24.7",
|
"@babel/preset-react": "7.24.7",
|
||||||
"@rollup/plugin-replace": "5.0.7",
|
"@rollup/plugin-replace": "5.0.7",
|
||||||
"@rollup/pluginutils": "5.1.0",
|
"@rollup/pluginutils": "5.1.0",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.4",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@vaadin/hilla-generator-cli": "24.4.6",
|
"@vaadin/hilla-generator-cli": "24.4.7",
|
||||||
"@vaadin/hilla-generator-core": "24.4.6",
|
"@vaadin/hilla-generator-core": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-backbone": "24.4.6",
|
"@vaadin/hilla-generator-plugin-backbone": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-barrel": "24.4.6",
|
"@vaadin/hilla-generator-plugin-barrel": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-client": "24.4.6",
|
"@vaadin/hilla-generator-plugin-client": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-model": "24.4.6",
|
"@vaadin/hilla-generator-plugin-model": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-push": "24.4.6",
|
"@vaadin/hilla-generator-plugin-push": "24.4.7",
|
||||||
"@vaadin/hilla-generator-plugin-subtypes": "24.4.6",
|
"@vaadin/hilla-generator-plugin-subtypes": "24.4.7",
|
||||||
"@vaadin/hilla-generator-utils": "24.4.6",
|
"@vaadin/hilla-generator-utils": "24.4.7",
|
||||||
"@vitejs/plugin-react": "4.3.1",
|
"@vitejs/plugin-react": "4.3.1",
|
||||||
"async": "3.2.5",
|
"async": "3.2.6",
|
||||||
"glob": "10.4.1",
|
"glob": "10.4.5",
|
||||||
"rollup-plugin-brotli": "3.1.0",
|
"rollup-plugin-brotli": "3.1.0",
|
||||||
"rollup-plugin-visualizer": "5.12.0",
|
"rollup-plugin-visualizer": "5.12.0",
|
||||||
"strip-css-comments": "5.0.0",
|
"strip-css-comments": "5.0.0",
|
||||||
"transform-ast": "2.4.4",
|
"transform-ast": "2.4.4",
|
||||||
"typescript": "5.4.5",
|
"typescript": "5.4.5",
|
||||||
"vite": "5.3.3",
|
"vite": "5.4.2",
|
||||||
"vite-plugin-checker": "0.6.4",
|
"vite-plugin-checker": "0.6.4",
|
||||||
"workbox-build": "7.1.1",
|
"workbox-build": "7.1.1",
|
||||||
"workbox-core": "7.1.0",
|
"workbox-core": "7.1.0",
|
||||||
"workbox-precaching": "7.1.0"
|
"workbox-precaching": "7.1.0"
|
||||||
},
|
},
|
||||||
"hash": "4a2abb2b0dc544e07b47be9cd538fec640d2646604044d77bd9ce5feecf4c3b3"
|
"hash": "e4807cd1e75275abaaf1e41e23a8662d8fe4d7fb38029306089a3997a813d25e"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@ne
|
|||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
export default function ProfileMenu() {
|
export default function ProfileMenu() {
|
||||||
const {state, logout} = useAuth();
|
const auth = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const profileMenuItems = [
|
const profileMenuItems = [
|
||||||
@@ -17,7 +17,7 @@ export default function ProfileMenu() {
|
|||||||
label: "Administration",
|
label: "Administration",
|
||||||
icon: <GearFine/>,
|
icon: <GearFine/>,
|
||||||
onClick: () => navigate("/administration/libraries"),
|
onClick: () => navigate("/administration/libraries"),
|
||||||
showIf: state.user?.roles?.some(a => a?.includes("ADMIN"))
|
showIf: auth.state.user?.roles?.some(a => a?.includes("ADMIN"))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Help",
|
label: "Help",
|
||||||
@@ -27,7 +27,7 @@ export default function ProfileMenu() {
|
|||||||
{
|
{
|
||||||
label: "Sign Out",
|
label: "Sign Out",
|
||||||
icon: <SignOut/>,
|
icon: <SignOut/>,
|
||||||
onClick: () => logout(),
|
onClick: () => auth.logout(),
|
||||||
color: "primary"
|
color: "primary"
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -36,6 +36,7 @@ export default function ProfileMenu() {
|
|||||||
<Dropdown placement="bottom-end">
|
<Dropdown placement="bottom-end">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar showFallback
|
<Avatar showFallback
|
||||||
|
src={`/images/avatar?username=${auth.state.user?.username}`}
|
||||||
radius="full"
|
radius="full"
|
||||||
as="button"
|
as="button"
|
||||||
className="transition-transform size-8"
|
className="transition-transform size-8"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Section from "Frontend/components/general/Section";
|
import Section from "Frontend/components/general/Section";
|
||||||
import Input from "Frontend/components/general/Input";
|
import Input from "Frontend/components/general/Input";
|
||||||
import {Form, Formik} from "formik";
|
import {Form, Formik} from "formik";
|
||||||
import {Button} from "@nextui-org/react";
|
import {Avatar, Button} from "@nextui-org/react";
|
||||||
import {Check, Info} from "@phosphor-icons/react";
|
import {Check, Info} from "@phosphor-icons/react";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
import {useAuth} from "Frontend/util/auth";
|
import {useAuth} from "Frontend/util/auth";
|
||||||
@@ -10,6 +10,7 @@ import UserUpdateDto from "Frontend/generated/de/grimsi/gameyfin/users/dto/UserU
|
|||||||
import {UserEndpoint} from "Frontend/generated/endpoints";
|
import {UserEndpoint} from "Frontend/generated/endpoints";
|
||||||
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
|
import FileUpload from "Frontend/components/general/FileUpload";
|
||||||
|
|
||||||
export default function ProfileManagement() {
|
export default function ProfileManagement() {
|
||||||
const [configSaved, setConfigSaved] = useState(false);
|
const [configSaved, setConfigSaved] = useState(false);
|
||||||
@@ -90,13 +91,18 @@ export default function ProfileManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row flex-1 justify-between gap-8">
|
<div className="flex flex-row flex-1 justify-between gap-8">
|
||||||
|
<div className="flex flex-col basis-1/4 items-center">
|
||||||
|
<Section title="Avatar"></Section>
|
||||||
|
<Avatar showFallback
|
||||||
|
src={`/images/avatar?username=${auth.state.user?.username}`}
|
||||||
|
className="size-40 m-4"></Avatar>
|
||||||
|
<FileUpload upload="/avatar/upload" clear="/avatar/delete" accept="image/*"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col flex-grow">
|
<div className="flex flex-col flex-grow">
|
||||||
<Section title="Personal information"/>
|
<Section title="Personal information"/>
|
||||||
<Input name="username" label="Username" type="text" autocomplete="username"/>
|
<Input name="username" label="Username" type="text" autocomplete="username"/>
|
||||||
<Input name="email" label="Email" type="email" autocomplete="email"/>
|
<Input name="email" label="Email" type="email" autocomplete="email"/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col flex-1">
|
|
||||||
<Section title="Security"/>
|
<Section title="Security"/>
|
||||||
<Input name="newPassword" label="New Password" type="password"
|
<Input name="newPassword" label="New Password" type="password"
|
||||||
autocomplete="new-password"/>
|
autocomplete="new-password"/>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function UserManagementLayout({getConfig, formik}: any) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section title="Users"/>
|
<Section title="Users"/>
|
||||||
<div className="grid grid-flow-col grid-cols-4 gap-4">
|
<div className="grid grid-cols-300px gap-4">
|
||||||
{users.map((user) => <UserCard user={user} key={user.username}/>)}
|
{users.map((user) => <UserCard user={user} key={user.username}/>)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import {toast} from "sonner";
|
||||||
|
import {getCsrfToken} from "Frontend/util/auth";
|
||||||
|
import {Button, Input, Tooltip} from "@nextui-org/react";
|
||||||
|
import {useState} from "react";
|
||||||
|
import {Trash} from "@phosphor-icons/react";
|
||||||
|
|
||||||
|
export default function FileUpload({upload, clear, accept}: { upload: string, clear: string, accept: string }) {
|
||||||
|
|
||||||
|
const [avatar, setAvatar] = useState<any>();
|
||||||
|
|
||||||
|
function onFileSelected(event: any) {
|
||||||
|
setAvatar(event.target.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadAvatar() {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("file", avatar);
|
||||||
|
try {
|
||||||
|
const response = await fetch(upload, {
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-Token": getCsrfToken()
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.text();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success("Avatar updated");
|
||||||
|
} else {
|
||||||
|
toast.error("Error uploading avatar", {description: result});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error("Error uploading avatar", {description: error.message})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeAvatar() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(clear, {
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-Token": getCsrfToken()
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin"
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.text();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success("Avatar removed");
|
||||||
|
} else {
|
||||||
|
toast.error("Error removing avatar", {description: result});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error("Error removing avatar", {description: error.message})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Input type="file" accept={accept} onChange={onFileSelected}/>
|
||||||
|
<Button onClick={uploadAvatar} color="success">Upload</Button>
|
||||||
|
<Tooltip content="Remove your current avatar">
|
||||||
|
<Button onClick={removeAvatar} isIconOnly color="danger"><Trash/></Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import {Divider} from "@nextui-org/react";
|
|||||||
export default function Section({title}: { title: string }) {
|
export default function Section({title}: { title: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h2 className={"text-xl font-bold mt-8"}>{title}</h2>
|
<h2 className={"text-xl font-bold mt-8 mb-1"}>{title}</h2>
|
||||||
<Divider className="mb-4"/>
|
<Divider className="mb-4"/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import {roleToColor, roleToRoleName} from "Frontend/util/utils";
|
|||||||
|
|
||||||
export function UserCard({user}: { user: UserInfoDto }) {
|
export function UserCard({user}: { user: UserInfoDto }) {
|
||||||
return (
|
return (
|
||||||
<Card className="flex flex-row flex-grow items-center gap-4 p-2">
|
<Card className="flex flex-row items-center gap-4 p-2">
|
||||||
<Avatar classNames={{
|
<Avatar showFallback
|
||||||
base: "gradient-primary size-20",
|
name={user.username?.charAt(0)}
|
||||||
icon: "text-background/80"
|
src={`/images/avatar?username=${user?.username}`}
|
||||||
}}></Avatar>
|
classNames={{
|
||||||
|
base: "gradient-primary size-20",
|
||||||
|
icon: "text-background/80",
|
||||||
|
name: "text-background/80 text-5xl -mt-1",
|
||||||
|
}}></Avatar>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="font-semibold">{user.username}</p>
|
<p className="font-semibold">{user.username}</p>
|
||||||
<p className="text-sm">{user.email}</p>
|
<p className="text-sm">{user.email}</p>
|
||||||
|
|||||||
@@ -8,3 +8,9 @@ const auth = configureAuth(UserEndpoint.getUserInfo);
|
|||||||
// typed to the result of `UserInfoService.getUserInfo`
|
// typed to the result of `UserInfoService.getUserInfo`
|
||||||
export const useAuth = auth.useAuth;
|
export const useAuth = auth.useAuth;
|
||||||
export const AuthProvider = auth.AuthProvider;
|
export const AuthProvider = auth.AuthProvider;
|
||||||
|
|
||||||
|
|
||||||
|
export function getCsrfToken() {
|
||||||
|
const token = document.querySelector('meta[name="_csrf"]')?.getAttribute('content');
|
||||||
|
return token || '';
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ class SecurityConfig(
|
|||||||
http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry ->
|
http.authorizeHttpRequests { auth: AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry ->
|
||||||
auth.requestMatchers("/setup").permitAll()
|
auth.requestMatchers("/setup").permitAll()
|
||||||
.requestMatchers("/public/**").permitAll()
|
.requestMatchers("/public/**").permitAll()
|
||||||
|
.requestMatchers("/images/**").permitAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
http.sessionManagement { sessionManagement ->
|
http.sessionManagement { sessionManagement ->
|
||||||
@@ -45,7 +46,6 @@ class SecurityConfig(
|
|||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
public override fun configure(web: WebSecurity) {
|
public override fun configure(web: WebSecurity) {
|
||||||
super.configure(web)
|
super.configure(web)
|
||||||
web.ignoring().requestMatchers("/images/**")
|
|
||||||
|
|
||||||
if ("dev" in environment.activeProfiles) {
|
if ("dev" in environment.activeProfiles) {
|
||||||
web.ignoring().requestMatchers("/h2-console/**")
|
web.ignoring().requestMatchers("/h2-console/**")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import jakarta.annotation.security.RolesAllowed
|
|||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
class UserEndpoint(
|
class UserEndpoint(
|
||||||
private val userService: UserService
|
private val userService: UserService
|
||||||
@@ -22,7 +23,6 @@ class UserEndpoint(
|
|||||||
return userService.getUserInfo(auth.name)
|
return userService.getUserInfo(auth.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PermitAll
|
|
||||||
@RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN)
|
@RolesAllowed(Roles.Names.SUPERADMIN, Roles.Names.ADMIN)
|
||||||
fun getAllUsers(): List<UserInfoDto> {
|
fun getAllUsers(): List<UserInfoDto> {
|
||||||
return userService.getAllUsers()
|
return userService.getAllUsers()
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package de.grimsi.gameyfin.users
|
|||||||
import de.grimsi.gameyfin.meta.Roles
|
import de.grimsi.gameyfin.meta.Roles
|
||||||
import de.grimsi.gameyfin.users.dto.UserInfoDto
|
import de.grimsi.gameyfin.users.dto.UserInfoDto
|
||||||
import de.grimsi.gameyfin.users.dto.UserUpdateDto
|
import de.grimsi.gameyfin.users.dto.UserUpdateDto
|
||||||
|
import de.grimsi.gameyfin.users.entities.Avatar
|
||||||
import de.grimsi.gameyfin.users.entities.Role
|
import de.grimsi.gameyfin.users.entities.Role
|
||||||
import de.grimsi.gameyfin.users.entities.User
|
import de.grimsi.gameyfin.users.entities.User
|
||||||
|
import de.grimsi.gameyfin.users.persistence.AvatarContentStore
|
||||||
import de.grimsi.gameyfin.users.persistence.UserRepository
|
import de.grimsi.gameyfin.users.persistence.UserRepository
|
||||||
import jakarta.transaction.Transactional
|
import jakarta.transaction.Transactional
|
||||||
import org.springframework.security.core.GrantedAuthority
|
import org.springframework.security.core.GrantedAuthority
|
||||||
@@ -14,6 +16,8 @@ import org.springframework.security.core.userdetails.UserDetailsService
|
|||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.web.multipart.MultipartFile
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -22,7 +26,8 @@ class UserService(
|
|||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val passwordEncoder: PasswordEncoder,
|
private val passwordEncoder: PasswordEncoder,
|
||||||
private val roleService: RoleService,
|
private val roleService: RoleService,
|
||||||
private val sessionService: SessionService
|
private val sessionService: SessionService,
|
||||||
|
private val avatarStore: AvatarContentStore
|
||||||
) : UserDetailsService {
|
) : UserDetailsService {
|
||||||
|
|
||||||
override fun loadUserByUsername(username: String): UserDetails {
|
override fun loadUserByUsername(username: String): UserDetails {
|
||||||
@@ -50,6 +55,35 @@ class UserService(
|
|||||||
return toUserInfo(user)
|
return toUserInfo(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAvatar(username: String): Avatar? {
|
||||||
|
val user = userByUsername(username)
|
||||||
|
return user.avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAvatarFile(avatar: Avatar): InputStream {
|
||||||
|
return avatarStore.getContent(avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setAvatar(username: String, file: MultipartFile) {
|
||||||
|
val user = userByUsername(username)
|
||||||
|
|
||||||
|
if (user.avatar == null) {
|
||||||
|
user.avatar = Avatar(mimeType = file.contentType)
|
||||||
|
}
|
||||||
|
|
||||||
|
avatarStore.setContent(user.avatar, file.inputStream)
|
||||||
|
userRepository.save(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAvatar(username: String) {
|
||||||
|
val user = userByUsername(username)
|
||||||
|
|
||||||
|
avatarStore.unsetContent(user.avatar)
|
||||||
|
user.avatar = null
|
||||||
|
|
||||||
|
userRepository.save(user)
|
||||||
|
}
|
||||||
|
|
||||||
fun registerUser(user: User, role: Roles): User {
|
fun registerUser(user: User, role: Roles): User {
|
||||||
return registerUser(user, listOf(role))
|
return registerUser(user, listOf(role))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package de.grimsi.gameyfin.users.avatar
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.users.UserService
|
||||||
|
import jakarta.annotation.security.PermitAll
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.core.io.InputStreamResource
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.core.Authentication
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import org.springframework.web.multipart.MultipartFile
|
||||||
|
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class AvatarController(
|
||||||
|
private val userService: UserService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping("/avatar/upload")
|
||||||
|
fun uploadAvatar(@RequestParam("file") file: MultipartFile) {
|
||||||
|
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
||||||
|
userService.setAvatar(auth.name, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/avatar/delete")
|
||||||
|
fun deleteAvatar() {
|
||||||
|
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
||||||
|
userService.deleteAvatar(auth.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermitAll
|
||||||
|
@GetMapping("/images/avatar")
|
||||||
|
fun getAvatar(
|
||||||
|
@RequestParam("username") username: String,
|
||||||
|
response: HttpServletResponse
|
||||||
|
): ResponseEntity<InputStreamResource>? {
|
||||||
|
val avatar = userService.getAvatar(username) ?: return ResponseEntity.notFound().build()
|
||||||
|
|
||||||
|
val file = avatar.let { userService.getAvatarFile(it) }
|
||||||
|
|
||||||
|
val inputStreamResource = InputStreamResource(file)
|
||||||
|
val headers = HttpHeaders()
|
||||||
|
headers.contentLength = avatar.contentLength!!
|
||||||
|
headers.contentType = MediaType.parseMediaType(avatar.mimeType!!)
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.headers(headers)
|
||||||
|
.body(inputStreamResource)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,16 +8,16 @@ import org.springframework.content.commons.annotations.MimeType
|
|||||||
|
|
||||||
|
|
||||||
@Embeddable
|
@Embeddable
|
||||||
class Avatar {
|
class Avatar(
|
||||||
@ContentId
|
@ContentId
|
||||||
@Nullable
|
@Nullable
|
||||||
var contentId: String? = null
|
var contentId: String? = null,
|
||||||
|
|
||||||
@ContentLength
|
@ContentLength
|
||||||
@Nullable
|
@Nullable
|
||||||
var contentLength: Long? = null
|
var contentLength: Long? = null,
|
||||||
|
|
||||||
@MimeType
|
@MimeType
|
||||||
@Nullable
|
@Nullable
|
||||||
var mimeType: String? = null
|
var mimeType: String? = null
|
||||||
}
|
)
|
||||||
@@ -20,9 +20,9 @@ spring:
|
|||||||
devtools.restart.additional-exclude: dev/hilla/openapi.json
|
devtools.restart.additional-exclude: dev/hilla/openapi.json
|
||||||
jpa:
|
jpa:
|
||||||
# defer-datasource-initialization: true
|
# defer-datasource-initialization: true
|
||||||
database-platform: org.hibernate.dialect.H2Dialect
|
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
ddl-auto: update
|
||||||
|
open-in-view: false
|
||||||
mustache:
|
mustache:
|
||||||
check-template-location: false
|
check-template-location: false
|
||||||
sql.init.mode: always
|
sql.init.mode: always
|
||||||
@@ -32,6 +32,8 @@ spring:
|
|||||||
db-name: gameyfin_db
|
db-name: gameyfin_db
|
||||||
url: jdbc:h2:file:./db/${spring.datasource.db-name}
|
url: jdbc:h2:file:./db/${spring.datasource.db-name}
|
||||||
driverClassName: org.h2.Driver
|
driverClassName: org.h2.Driver
|
||||||
|
content:
|
||||||
|
fs.filesystem-root: ./data/
|
||||||
application:
|
application:
|
||||||
name: Gameyfin
|
name: Gameyfin
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export default withMT({
|
|||||||
colors: {
|
colors: {
|
||||||
'gf-primary': '#2332c8',
|
'gf-primary': '#2332c8',
|
||||||
'gf-secondary': '#6441a5'
|
'gf-secondary': '#6441a5'
|
||||||
|
},
|
||||||
|
gridTemplateColumns: {
|
||||||
|
'300px': 'repeat(auto-fit, 300px)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user