Layout updates

Added more themed components
Refactored Superadmin creation
This commit is contained in:
grimsi
2024-05-15 19:43:41 +02:00
parent 215a01606f
commit a61c8eead0
13 changed files with 87 additions and 62 deletions
+1 -1
View File
@@ -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>
+11 -5
View File
@@ -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
+2 -20
View File
@@ -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>
*/
+1 -1
View File
@@ -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);
+11 -10
View File
@@ -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/>
</div> </NavbarItem>
</Card> </NavbarContent>
</Navbar>
<Outlet/> <Outlet/>
</> </>
+15 -5
View File
@@ -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')
+1 -1
View File
@@ -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>
) )
+1 -1
View File
@@ -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