mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Layout updates
Added more themed components Refactored Superadmin creation
This commit is contained in:
@@ -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,11 +1,9 @@
|
|||||||
import {useAuth} from "Frontend/util/auth";
|
import {useAuth} from "Frontend/util/auth";
|
||||||
import {useNavigate} from "react-router-dom";
|
|
||||||
import {GearFine, Question, SignOut, User} from "@phosphor-icons/react";
|
import {GearFine, Question, SignOut, User} from "@phosphor-icons/react";
|
||||||
import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@nextui-org/react";
|
import {Avatar, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger} from "@nextui-org/react";
|
||||||
|
|
||||||
export default function ProfileMenu() {
|
export default function ProfileMenu() {
|
||||||
const {state, logout} = useAuth();
|
const {state, logout} = useAuth();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const profileMenuItems = [
|
const profileMenuItems = [
|
||||||
{
|
{
|
||||||
@@ -17,7 +15,7 @@ export default function ProfileMenu() {
|
|||||||
label: "Administration",
|
label: "Administration",
|
||||||
icon: <GearFine/>,
|
icon: <GearFine/>,
|
||||||
onClick: () => alert("Administration"),
|
onClick: () => alert("Administration"),
|
||||||
showIf: state.user?.authorities?.some(a => a?.includes("ADMIN"))
|
showIf: state.user?.roles?.some(a => a?.includes("ADMIN"))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Help",
|
label: "Help",
|
||||||
@@ -35,7 +33,15 @@ export default function ProfileMenu() {
|
|||||||
return (
|
return (
|
||||||
<Dropdown placement="bottom-end">
|
<Dropdown placement="bottom-end">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar showFallback radius="full" as="button" className="transition-transform"/>
|
<Avatar showFallback
|
||||||
|
radius="full"
|
||||||
|
as="button"
|
||||||
|
className="transition-transform size-8"
|
||||||
|
classNames={{
|
||||||
|
base: "bg-gradient-to-br from-primary-400 to-primary-700",
|
||||||
|
icon: "text-background/80"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
@@ -48,7 +54,7 @@ export default function ProfileMenu() {
|
|||||||
startContent={<div color={color}>{icon}</div>}
|
startContent={<div color={color}>{icon}</div>}
|
||||||
/* @ts-ignore */
|
/* @ts-ignore */
|
||||||
color={color ? color : ""}
|
color={color ? color : ""}
|
||||||
className={`text-${color}`}
|
className={`text-${color} hover:bg-primary/20`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</DropdownItem> : null
|
</DropdownItem> : null
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {Theme} from "Frontend/theming/theme";
|
import {Theme} from "Frontend/theming/theme";
|
||||||
import {Card, Tooltip} from "@nextui-org/react";
|
import {Tooltip} from "@nextui-org/react";
|
||||||
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
|
||||||
|
|
||||||
export default function ThemePreview({theme, mode, isSelected}: {
|
export default function ThemePreview({theme, mode, isSelected}: {
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
@@ -11,26 +10,9 @@ export default function ThemePreview({theme, mode, isSelected}: {
|
|||||||
<Tooltip content={<p className="capitalize">{theme.name?.replace("-", " ")}</p>} placement="bottom">
|
<Tooltip content={<p className="capitalize">{theme.name?.replace("-", " ")}</p>} placement="bottom">
|
||||||
<div className={`
|
<div className={`
|
||||||
${theme.name}-${mode}
|
${theme.name}-${mode}
|
||||||
bg-primary p-6 border-2 rounded-md
|
bg-primary p-6 border-2 rounded-full
|
||||||
${isSelected ? "border-foreground" : "border-foreground-200 hover:border-focus"}`}
|
${isSelected ? "border-foreground" : "border-foreground-200 hover:border-focus"}`}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path id="background" d="M0 0H228V120H0V0Z" fill={theme.cssVars[resolvedTheme].background}/>
|
|
||||||
<rect id="background-secondary" x="29" y="54" width="144" height="53" rx="2" fill="#30363D"/>
|
|
||||||
<rect x="184" y="54" width="32" height="36" rx="2" fill="#30363D"/>
|
|
||||||
<rect opacity="0.3" x="29" y="59" width="144" height="12" fill="#2EA043"/>
|
|
||||||
<path opacity="0.6" d="M0 0H228V23H0V0Z" fill="#484F58"/>
|
|
||||||
<rect x="13" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
|
|
||||||
<rect x="29" y="36" width="48" height="6" rx="3" fill="#6E7681"/>
|
|
||||||
<rect x="34" y="62" width="64" height="6" rx="3" fill="#3FB950"/>
|
|
||||||
<rect x="210" y="36" width="6" height="6" rx="1" fill="#DA3633"/>
|
|
||||||
<rect x="202" y="36" width="6" height="6" rx="1" fill="#3FB950"/>
|
|
||||||
<rect x="53" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
|
|
||||||
<rect x="93" y="9" width="32" height="6" rx="3" fill="#8B949E"/>
|
|
||||||
</svg>
|
|
||||||
*/
|
|
||||||
@@ -7,7 +7,7 @@ import {Step, Stepper} from "@material-tailwind/react";
|
|||||||
const Wizard = ({children, initialValues, onSubmit}: {
|
const Wizard = ({children, initialValues, onSubmit}: {
|
||||||
children: ReactNode,
|
children: ReactNode,
|
||||||
initialValues: any,
|
initialValues: any,
|
||||||
onSubmit: (values: any, bag: FormikHelpers<any> | FormikBag<any, any>) => Promise<void>
|
onSubmit: (values: any, bag: FormikHelpers<any> | FormikBag<any, any>) => Promise<any>
|
||||||
}) => {
|
}) => {
|
||||||
const [stepNumber, setStepNumber] = useState(0);
|
const [stepNumber, setStepNumber] = useState(0);
|
||||||
const steps = React.Children.toArray(children);
|
const steps = React.Children.toArray(children);
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import {useRouteMetadata} from 'Frontend/util/routing.js';
|
|||||||
import {useEffect} from 'react';
|
import {useEffect} from 'react';
|
||||||
import ProfileMenu from "Frontend/components/ProfileMenu";
|
import ProfileMenu from "Frontend/components/ProfileMenu";
|
||||||
import {Outlet} from "react-router-dom";
|
import {Outlet} from "react-router-dom";
|
||||||
import {Card} from "@nextui-org/react";
|
import {Navbar, NavbarBrand, NavbarContent, NavbarItem} from "@nextui-org/react";
|
||||||
|
import GameyfinLogo from "Frontend/components/theming/GameyfinLogo";
|
||||||
|
|
||||||
export default function MainLayout() {
|
export default function MainLayout() {
|
||||||
const currentTitle = `Gameyfin - ${useRouteMetadata()?.title}` ?? 'Gameyfin';
|
const currentTitle = `Gameyfin - ${useRouteMetadata()?.title}` ?? 'Gameyfin';
|
||||||
@@ -12,16 +13,16 @@ export default function MainLayout() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="sticky top-0 z-10 h-max max-w-full rounded-none px-4 py-2">
|
<Navbar maxWidth="2xl" className="shadow">
|
||||||
<div className="flex items-center justify-end text-blue-gray-900">
|
<NavbarBrand>
|
||||||
<img
|
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||||
className="absolute w-full content-center h-8"
|
</NavbarBrand>
|
||||||
src="/images/Logo.svg"
|
<NavbarContent justify="end">
|
||||||
alt="Gameyfin"
|
<NavbarItem>
|
||||||
/>
|
<ProfileMenu/>
|
||||||
<ProfileMenu/>
|
</NavbarItem>
|
||||||
</div>
|
</NavbarContent>
|
||||||
</Card>
|
</Navbar>
|
||||||
|
|
||||||
<Outlet/>
|
<Outlet/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {themes} from "Frontend/theming/themes";
|
|||||||
import {Card, Switch} from "@nextui-org/react";
|
import {Card, Switch} from "@nextui-org/react";
|
||||||
import {useTheme} from "next-themes";
|
import {useTheme} from "next-themes";
|
||||||
import {Theme} from "Frontend/theming/theme";
|
import {Theme} from "Frontend/theming/theme";
|
||||||
|
import {UserEndpoint} from "Frontend/generated/endpoints";
|
||||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
function WelcomeStep() {
|
function WelcomeStep() {
|
||||||
return (
|
return (
|
||||||
@@ -95,6 +94,11 @@ function UserStep() {
|
|||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
label="E-Mail"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
@@ -126,10 +130,13 @@ const SetupView = () => (
|
|||||||
<div className="flex size-full bg-gradient-to-br from-primary-400 to-primary-700">
|
<div className="flex size-full bg-gradient-to-br from-primary-400 to-primary-700">
|
||||||
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8">
|
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8">
|
||||||
<Wizard
|
<Wizard
|
||||||
initialValues={{username: '', password: '', passwordRepeat: ''}}
|
initialValues={{username: '', email: '', password: '', passwordRepeat: ''}}
|
||||||
onSubmit={async (values: any) =>
|
onSubmit={(values: any) => UserEndpoint.registerInitialSuperAdmin({
|
||||||
sleep(300).then(() => alert(JSON.stringify(values, null, 2)))
|
username: values.username,
|
||||||
}
|
password: values.password,
|
||||||
|
email: values.email
|
||||||
|
}
|
||||||
|
).then(() => alert("Successfully registered!"))}
|
||||||
>
|
>
|
||||||
<WizardStep icon={<HandWaving/>}>
|
<WizardStep icon={<HandWaving/>}>
|
||||||
<WelcomeStep/>
|
<WelcomeStep/>
|
||||||
@@ -144,6 +151,9 @@ const SetupView = () => (
|
|||||||
password: Yup.string()
|
password: Yup.string()
|
||||||
.min(8, 'Password must be at least 8 characters long')
|
.min(8, 'Password must be at least 8 characters long')
|
||||||
.required('Required'),
|
.required('Required'),
|
||||||
|
email: Yup.string()
|
||||||
|
.email()
|
||||||
|
.required('Required'),
|
||||||
passwordRepeat: Yup.string()
|
passwordRepeat: Yup.string()
|
||||||
.equals([Yup.ref('password')], 'Passwords do not match')
|
.equals([Yup.ref('password')], 'Passwords do not match')
|
||||||
.required('Required')
|
.required('Required')
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Link} from "react-router-dom";
|
|||||||
|
|
||||||
export default function TestView() {
|
export default function TestView() {
|
||||||
return (
|
return (
|
||||||
<div className="size-full flex justify-center">
|
<div className="flex grow justify-center">
|
||||||
<Link to="/setup">Setup</Link>
|
<Link to="/setup">Setup</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.users
|
package de.grimsi.gameyfin.users
|
||||||
|
|
||||||
import de.grimsi.gameyfin.config.Roles
|
import de.grimsi.gameyfin.config.Roles
|
||||||
|
import de.grimsi.gameyfin.users.entities.Role
|
||||||
import de.grimsi.gameyfin.users.persistence.RoleRepository
|
import de.grimsi.gameyfin.users.persistence.RoleRepository
|
||||||
import jakarta.transaction.Transactional
|
import jakarta.transaction.Transactional
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -18,4 +19,12 @@ class RoleService(
|
|||||||
val r = roleRepository.findByRolename(role.roleName) ?: return 0
|
val r = roleRepository.findByRolename(role.roleName) ?: return 0
|
||||||
return r.users.size
|
return r.users.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toRoles(roles: Collection<String>): List<Role> {
|
||||||
|
return roles.mapNotNull { r -> roleRepository.findByRolename(r) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toRole(role: String): Role {
|
||||||
|
return roleRepository.findByRolename(role) ?: throw RuntimeException("Role $role does not exist")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,37 +1,51 @@
|
|||||||
package de.grimsi.gameyfin.users
|
package de.grimsi.gameyfin.users
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.config.Roles
|
||||||
import de.grimsi.gameyfin.users.dto.UserInfo
|
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||||
import de.grimsi.gameyfin.users.dto.UserRegistration
|
import de.grimsi.gameyfin.users.dto.UserRegistration
|
||||||
import de.grimsi.gameyfin.users.entities.User
|
import de.grimsi.gameyfin.users.entities.User
|
||||||
import dev.hilla.Endpoint
|
import dev.hilla.Endpoint
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
import org.springframework.security.core.Authentication
|
import org.springframework.security.core.Authentication
|
||||||
import org.springframework.security.core.GrantedAuthority
|
import org.springframework.security.core.GrantedAuthority
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.web.server.ResponseStatusException
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
class UserEndpoint(
|
class UserEndpoint(
|
||||||
private val userService: UserService
|
private val userService: UserService,
|
||||||
|
private val roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun getUserInfo(): UserInfo {
|
fun getUserInfo(): UserInfo {
|
||||||
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
val auth: Authentication = SecurityContextHolder.getContext().authentication
|
||||||
val authorities: List<String> = auth.authorities.map { g: GrantedAuthority -> g.authority }
|
val authorities: List<String> = auth.authorities.map { g: GrantedAuthority -> g.authority }
|
||||||
return UserInfo(auth.name, authorities)
|
return UserInfo(username = auth.name, roles = authorities)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun registerUser(registration: UserRegistration): Boolean {
|
fun registerUser(registration: UserRegistration): UserInfo {
|
||||||
|
val user: User = registerUser(registration, listOf(Roles.USER))
|
||||||
|
return userService.toUserInfo(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PermitAll
|
||||||
|
fun registerInitialSuperAdmin(registration: UserRegistration): UserInfo {
|
||||||
|
if (roleService.getUserCountForRole(Roles.SUPERADMIN) > 0) throw ResponseStatusException(HttpStatus.UNAUTHORIZED)
|
||||||
|
val superAdmin: User = registerUser(registration, listOf(Roles.SUPERADMIN))
|
||||||
|
return userService.toUserInfo(superAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun registerUser(registration: UserRegistration, roles: List<Roles>): User {
|
||||||
val user = User(
|
val user = User(
|
||||||
username = registration.username,
|
username = registration.username,
|
||||||
password = registration.password,
|
password = registration.password,
|
||||||
email = registration.email,
|
email = registration.email,
|
||||||
roles = userService.toRoles(registration.roles)
|
roles = roles.map { r -> roleService.toRole(r.roleName) }
|
||||||
)
|
)
|
||||||
|
|
||||||
userService.registerUser(user)
|
return userService.registerUser(user)
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package de.grimsi.gameyfin.users
|
package de.grimsi.gameyfin.users
|
||||||
|
|
||||||
|
import de.grimsi.gameyfin.users.dto.UserInfo
|
||||||
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.RoleRepository
|
|
||||||
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
|
||||||
@@ -18,7 +18,6 @@ import org.springframework.stereotype.Service
|
|||||||
@Transactional
|
@Transactional
|
||||||
class UserService(
|
class UserService(
|
||||||
private val userRepository: UserRepository,
|
private val userRepository: UserRepository,
|
||||||
private val roleRepository: RoleRepository,
|
|
||||||
private val passwordEncoder: PasswordEncoder
|
private val passwordEncoder: PasswordEncoder
|
||||||
) : UserDetailsService {
|
) : UserDetailsService {
|
||||||
|
|
||||||
@@ -42,8 +41,12 @@ class UserService(
|
|||||||
return userRepository.save(user)
|
return userRepository.save(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toRoles(roles: Collection<String>): List<Role> {
|
fun toUserInfo(user: User): UserInfo {
|
||||||
return roles.mapNotNull { r -> roleRepository.findByRolename(r) }
|
return UserInfo(
|
||||||
|
username = user.username,
|
||||||
|
email = user.email,
|
||||||
|
roles = user.roles.map { r -> r.rolename }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
|
private fun toAuthorities(roles: Collection<Role>): List<GrantedAuthority> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.users.dto
|
package de.grimsi.gameyfin.users.dto
|
||||||
|
|
||||||
data class UserInfo(
|
data class UserInfo(
|
||||||
val name: String,
|
val username: String,
|
||||||
val authorities: List<String>
|
val email: String? = null,
|
||||||
|
val roles: List<String>
|
||||||
)
|
)
|
||||||
@@ -3,6 +3,5 @@ package de.grimsi.gameyfin.users.dto
|
|||||||
data class UserRegistration(
|
data class UserRegistration(
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val email: String,
|
val email: String
|
||||||
val roles: List<String>
|
|
||||||
)
|
)
|
||||||
@@ -8,7 +8,7 @@ server:
|
|||||||
tracking-modes: cookie
|
tracking-modes: cookie
|
||||||
|
|
||||||
spring:
|
spring:
|
||||||
# Workaround for https://github.dev/hilla/issues/842
|
# Workaround for https://github.com/vaadin/hilla/issues/842
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user