womp
This commit is contained in:
parent
4151dd89ed
commit
92d569b9cd
11 changed files with 204 additions and 2 deletions
|
|
@ -23,11 +23,22 @@ public class AuthController {
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new user.
|
||||||
|
* @param request Registration details
|
||||||
|
* @return The registered user
|
||||||
|
*/
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public ResponseEntity<User> register(@RequestBody Dtos.RegisterRequest request) {
|
public ResponseEntity<User> register(@RequestBody Dtos.RegisterRequest request) {
|
||||||
|
// registering the user, hope they remember their password
|
||||||
return ResponseEntity.ok(authService.register(request));
|
return ResponseEntity.ok(authService.register(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs in a user.
|
||||||
|
* @param request Login credentials
|
||||||
|
* @return The user details if login succeeds
|
||||||
|
*/
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<User> login(@RequestBody Dtos.LoginRequest request) {
|
public ResponseEntity<User> login(@RequestBody Dtos.LoginRequest request) {
|
||||||
Authentication authentication = authenticationManager.authenticate(
|
Authentication authentication = authenticationManager.authenticate(
|
||||||
|
|
@ -37,16 +48,29 @@ public class AuthController {
|
||||||
return ResponseEntity.ok(authService.getUserByEmail(request.getEmail()));
|
return ResponseEntity.ok(authService.getUserByEmail(request.getEmail()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently authenticated user.
|
||||||
|
* @param principal The security principal
|
||||||
|
* @return The user
|
||||||
|
*/
|
||||||
@GetMapping("/me")
|
@GetMapping("/me")
|
||||||
public ResponseEntity<User> getCurrentUser(Principal principal) {
|
public ResponseEntity<User> getCurrentUser(Principal principal) {
|
||||||
return ResponseEntity.ok(authService.getUserByEmail(principal.getName()));
|
return ResponseEntity.ok(authService.getUserByEmail(principal.getName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the supervised rooms for the current user.
|
||||||
|
* @param request Room IDs
|
||||||
|
* @param principal The security principal
|
||||||
|
* @return The updated user
|
||||||
|
*/
|
||||||
@PutMapping("/profile/rooms")
|
@PutMapping("/profile/rooms")
|
||||||
public ResponseEntity<User> updateMyRooms(@RequestBody Dtos.UpdateRoomsRequest request, Principal principal) {
|
public ResponseEntity<User> updateMyRooms(@RequestBody Dtos.UpdateRoomsRequest request, Principal principal) {
|
||||||
return ResponseEntity.ok(authService.updateSupervisedRooms(principal.getName(), request.getRoomIds()));
|
return ResponseEntity.ok(authService.updateSupervisedRooms(principal.getName(), request.getRoomIds()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emergency endpoint to promote a user to ADMIN (Removed before production!)
|
// Emergency endpoint to promote a user to ADMIN (Removed before production!)
|
||||||
|
// seriously, delete this before going live or we are doomed
|
||||||
@PostMapping("/dev-promote-admin")
|
@PostMapping("/dev-promote-admin")
|
||||||
public ResponseEntity<User> promoteToAdmin(@RequestBody Dtos.LoginRequest request) { // Reusing LoginRequest for email/password check essentially or just email
|
public ResponseEntity<User> promoteToAdmin(@RequestBody Dtos.LoginRequest request) { // Reusing LoginRequest for email/password check essentially or just email
|
||||||
// Ideally we check a secret key, but for now we just allow promoting by email if password matches or just by email for simplicity in this stuck state
|
// Ideally we check a secret key, but for now we just allow promoting by email if password matches or just by email for simplicity in this stuck state
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package de.itsolutions.ticketsystem.controller;
|
||||||
|
|
||||||
|
import de.itsolutions.ticketsystem.dto.Dtos;
|
||||||
|
import de.itsolutions.ticketsystem.entity.User;
|
||||||
|
import de.itsolutions.ticketsystem.service.AuthService;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for stuff that relates to the user, but not necessarily login/register.
|
||||||
|
* Kept separate because clean code or whatever.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/users")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
public UserController(AuthService authService) {
|
||||||
|
this.authService = authService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the theme for the current user.
|
||||||
|
* @param request The theme update request
|
||||||
|
* @param principal The security principal (the logged in dude)
|
||||||
|
* @return The updated user
|
||||||
|
*/
|
||||||
|
@PatchMapping("/theme")
|
||||||
|
public ResponseEntity<User> updateTheme(@RequestBody Dtos.ThemeUpdateRequest request, Principal principal) {
|
||||||
|
// Just delegating to service, standard procedure
|
||||||
|
return ResponseEntity.ok(authService.updateTheme(principal.getName(), request.getTheme()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,4 +62,11 @@ public class Dtos {
|
||||||
public String getStatus() { return status; }
|
public String getStatus() { return status; }
|
||||||
public void setStatus(String status) { this.status = status; }
|
public void setStatus(String status) { this.status = status; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request payload for changing the vibe (theme)
|
||||||
|
public static class ThemeUpdateRequest {
|
||||||
|
private String theme;
|
||||||
|
public String getTheme() { return theme; }
|
||||||
|
public void setTheme(String theme) { this.theme = theme; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ public class User {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String role;
|
private String role;
|
||||||
|
|
||||||
|
@Column(name = "theme", nullable = false)
|
||||||
|
private String theme = "system";
|
||||||
|
|
||||||
@ManyToMany(fetch = FetchType.EAGER)
|
@ManyToMany(fetch = FetchType.EAGER)
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "user_rooms",
|
name = "user_rooms",
|
||||||
|
|
@ -50,12 +53,14 @@ public class User {
|
||||||
public void setId(Long id) { this.id = id; }
|
public void setId(Long id) { this.id = id; }
|
||||||
|
|
||||||
public String getFirstname() { return name_firstname; }
|
public String getFirstname() { return name_firstname; }
|
||||||
|
// setter for firstname, keeping legacy vorname in sync because reasons...
|
||||||
public void setFirstname(String firstname) {
|
public void setFirstname(String firstname) {
|
||||||
this.name_firstname = firstname;
|
this.name_firstname = firstname;
|
||||||
this.name_vorname = firstname;
|
this.name_vorname = firstname;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLastname() { return name_lastname; }
|
public String getLastname() { return name_lastname; }
|
||||||
|
// syncing lastname with nachname, database duplicates ftw
|
||||||
public void setLastname(String lastname) {
|
public void setLastname(String lastname) {
|
||||||
this.name_lastname = lastname;
|
this.name_lastname = lastname;
|
||||||
this.name_nachname = lastname;
|
this.name_nachname = lastname;
|
||||||
|
|
@ -65,6 +70,7 @@ public class User {
|
||||||
return this.name_column;
|
return this.name_column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// null check is good for the soul
|
||||||
public void setName(String name) {
|
public void setName(String name) {
|
||||||
if (name == null || name.isBlank()) {
|
if (name == null || name.isBlank()) {
|
||||||
name = "Unknown User";
|
name = "Unknown User";
|
||||||
|
|
@ -80,4 +86,8 @@ public class User {
|
||||||
public void setRole(String role) { this.role = role; }
|
public void setRole(String role) { this.role = role; }
|
||||||
public List<Room> getSupervisedRooms() { return supervisedRooms; }
|
public List<Room> getSupervisedRooms() { return supervisedRooms; }
|
||||||
public void setSupervisedRooms(List<Room> supervisedRooms) { this.supervisedRooms = supervisedRooms; }
|
public void setSupervisedRooms(List<Room> supervisedRooms) { this.supervisedRooms = supervisedRooms; }
|
||||||
|
|
||||||
|
// dark mode stuff, gotta save those eyes
|
||||||
|
public String getTheme() { return theme; }
|
||||||
|
public void setTheme(String theme) { this.theme = theme; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,21 @@ public class AuthService {
|
||||||
|
|
||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the user's theme preference.
|
||||||
|
* @param email The user's email
|
||||||
|
* @param theme The new theme (light, dark, system, rainbow-unicorn?)
|
||||||
|
* @return The updated user
|
||||||
|
*/
|
||||||
|
public User updateTheme(String email, String theme) {
|
||||||
|
User user = userRepository.findByEmail(email)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
// whatever theme they want, they get. no validation, yolo.
|
||||||
|
user.setTheme(theme);
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
public User promoteToAdmin(String email) {
|
public User promoteToAdmin(String email) {
|
||||||
User user = userRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found"));
|
User user = userRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Geist, Geist_Mono } from 'next/font/google'
|
||||||
import { Analytics } from '@vercel/analytics/next'
|
import { Analytics } from '@vercel/analytics/next'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
import { NextAuthSessionProvider } from "@/components/session-provider"
|
import { NextAuthSessionProvider } from "@/components/session-provider"
|
||||||
|
import { UserThemeSync } from "@/components/user-theme-sync"
|
||||||
|
|
||||||
const _geist = Geist({ subsets: ["latin"] });
|
const _geist = Geist({ subsets: ["latin"] });
|
||||||
const _geistMono = Geist_Mono({ subsets: ["latin"] });
|
const _geistMono = Geist_Mono({ subsets: ["latin"] });
|
||||||
|
|
@ -40,6 +41,7 @@ export default function RootLayout({
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`font-sans antialiased`}>
|
<body className={`font-sans antialiased`}>
|
||||||
<NextAuthSessionProvider>
|
<NextAuthSessionProvider>
|
||||||
|
<UserThemeSync />
|
||||||
{children}
|
{children}
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</NextAuthSessionProvider>
|
</NextAuthSessionProvider>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
||||||
setError("")
|
setError("")
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// let's hope they know their password lmao
|
||||||
const success = await login(email, password)
|
const success = await login(email, password)
|
||||||
if (!success) {
|
if (!success) {
|
||||||
setError("Ungültige E-Mail oder Passwort")
|
setError("Ungültige E-Mail oder Passwort")
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
||||||
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
|
// new user alert! make sure they don't break anything
|
||||||
const success = await register(
|
const success = await register(
|
||||||
{
|
{
|
||||||
firstname,
|
firstname,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { LogOut, User, Building2, Cpu } from "lucide-react"
|
import { LogOut, User, Building2, Cpu } from "lucide-react"
|
||||||
|
import { ModeToggle } from "@/components/mode-toggle"
|
||||||
|
|
||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
|
|
@ -89,13 +90,13 @@ export function AppShell({ children }: AppShellProps) {
|
||||||
<DropdownMenuSubContent className="max-h-60 overflow-y-auto">
|
<DropdownMenuSubContent className="max-h-60 overflow-y-auto">
|
||||||
<DropdownMenuLabel>Räume verwalten</DropdownMenuLabel>
|
<DropdownMenuLabel>Räume verwalten</DropdownMenuLabel>
|
||||||
{rooms.map((room) => {
|
{rooms.map((room) => {
|
||||||
const isChecked = user.supervisedRooms.some((r) => r.id === room.id)
|
const isChecked = user.supervisedRooms?.some((r) => r.id === room.id) ?? false
|
||||||
return (
|
return (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={room.id}
|
key={room.id}
|
||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
const currentIds = user.supervisedRooms.map((r) => r.id)
|
const currentIds = user.supervisedRooms?.map((r) => r.id) || []
|
||||||
let newIds
|
let newIds
|
||||||
if (checked) {
|
if (checked) {
|
||||||
newIds = [...currentIds, room.id]
|
newIds = [...currentIds, room.id]
|
||||||
|
|
@ -120,6 +121,9 @@ export function AppShell({ children }: AppShellProps) {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{/* dark mode toggle because staring at white screens hurts */}
|
||||||
|
<ModeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
36
Frontend/components/mode-toggle.tsx
Normal file
36
Frontend/components/mode-toggle.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
export function ModeToggle() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const [mounted, setMounted] = React.useState(false)
|
||||||
|
|
||||||
|
// Hydration fix
|
||||||
|
React.useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDark = theme === "dark"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Sun className="h-4 w-4" />
|
||||||
|
<Switch
|
||||||
|
id="dark-mode"
|
||||||
|
checked={isDark}
|
||||||
|
onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
|
||||||
|
/>
|
||||||
|
<Moon className="h-4 w-4" />
|
||||||
|
<Label htmlFor="dark-mode" className="sr-only">Toggle theme</Label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
67
Frontend/components/user-theme-sync.tsx
Normal file
67
Frontend/components/user-theme-sync.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { useEffect, useRef } from "react"
|
||||||
|
import { useSession } from "next-auth/react"
|
||||||
|
|
||||||
|
export function UserThemeSync() {
|
||||||
|
const { theme, setTheme } = useTheme()
|
||||||
|
const { data: session } = useSession()
|
||||||
|
const initialSyncDone = useRef(false)
|
||||||
|
|
||||||
|
// Sync from DB on load
|
||||||
|
useEffect(() => {
|
||||||
|
// If we have a user and haven't synced yet
|
||||||
|
if (session?.user && !initialSyncDone.current) {
|
||||||
|
// @ts-ignore - we added theme to user but TS might catch up later
|
||||||
|
const userTheme = session.user.theme
|
||||||
|
if (userTheme && userTheme !== "system") {
|
||||||
|
setTheme(userTheme)
|
||||||
|
}
|
||||||
|
initialSyncDone.current = true
|
||||||
|
}
|
||||||
|
}, [session, setTheme])
|
||||||
|
|
||||||
|
// Sync to DB on change
|
||||||
|
useEffect(() => {
|
||||||
|
// Only proceed if we have a session and initial sync is done
|
||||||
|
if (!session?.user || !initialSyncDone.current) return
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const currentDbTheme = session.user.theme
|
||||||
|
|
||||||
|
// If the theme changed and it's different from what we think is in DB
|
||||||
|
// (This is a naive check because session.user.theme won't update until next session fetch)
|
||||||
|
if (theme && theme !== currentDbTheme) {
|
||||||
|
const saveTheme = async () => {
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const authHeader = session.authHeader
|
||||||
|
|
||||||
|
await fetch(process.env.NEXT_PUBLIC_API_URL + "/api/users/theme", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": authHeader
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ theme }),
|
||||||
|
})
|
||||||
|
// Ideally we update the session here too, but that's complex.
|
||||||
|
// We just fire and forget.
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save theme, looks like backend is sleeping", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple debounce
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
saveTheme()
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [theme, session])
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue