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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new user.
|
||||
* @param request Registration details
|
||||
* @return The registered user
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<User> register(@RequestBody Dtos.RegisterRequest request) {
|
||||
// registering the user, hope they remember their password
|
||||
return ResponseEntity.ok(authService.register(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs in a user.
|
||||
* @param request Login credentials
|
||||
* @return The user details if login succeeds
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<User> login(@RequestBody Dtos.LoginRequest request) {
|
||||
Authentication authentication = authenticationManager.authenticate(
|
||||
|
|
@ -37,16 +48,29 @@ public class AuthController {
|
|||
return ResponseEntity.ok(authService.getUserByEmail(request.getEmail()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently authenticated user.
|
||||
* @param principal The security principal
|
||||
* @return The user
|
||||
*/
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<User> getCurrentUser(Principal principal) {
|
||||
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")
|
||||
public ResponseEntity<User> updateMyRooms(@RequestBody Dtos.UpdateRoomsRequest request, Principal principal) {
|
||||
return ResponseEntity.ok(authService.updateSupervisedRooms(principal.getName(), request.getRoomIds()));
|
||||
}
|
||||
|
||||
// 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")
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 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)
|
||||
private String role;
|
||||
|
||||
@Column(name = "theme", nullable = false)
|
||||
private String theme = "system";
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(
|
||||
name = "user_rooms",
|
||||
|
|
@ -50,12 +53,14 @@ public class User {
|
|||
public void setId(Long id) { this.id = id; }
|
||||
|
||||
public String getFirstname() { return name_firstname; }
|
||||
// setter for firstname, keeping legacy vorname in sync because reasons...
|
||||
public void setFirstname(String firstname) {
|
||||
this.name_firstname = firstname;
|
||||
this.name_vorname = firstname;
|
||||
}
|
||||
|
||||
public String getLastname() { return name_lastname; }
|
||||
// syncing lastname with nachname, database duplicates ftw
|
||||
public void setLastname(String lastname) {
|
||||
this.name_lastname = lastname;
|
||||
this.name_nachname = lastname;
|
||||
|
|
@ -65,6 +70,7 @@ public class User {
|
|||
return this.name_column;
|
||||
}
|
||||
|
||||
// null check is good for the soul
|
||||
public void setName(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
name = "Unknown User";
|
||||
|
|
@ -80,4 +86,8 @@ public class User {
|
|||
public void setRole(String role) { this.role = role; }
|
||||
public List<Room> getSupervisedRooms() { return 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 './globals.css'
|
||||
import { NextAuthSessionProvider } from "@/components/session-provider"
|
||||
import { UserThemeSync } from "@/components/user-theme-sync"
|
||||
|
||||
const _geist = Geist({ subsets: ["latin"] });
|
||||
const _geistMono = Geist_Mono({ subsets: ["latin"] });
|
||||
|
|
@ -40,6 +41,7 @@ export default function RootLayout({
|
|||
<html lang="en">
|
||||
<body className={`font-sans antialiased`}>
|
||||
<NextAuthSessionProvider>
|
||||
<UserThemeSync />
|
||||
{children}
|
||||
<Analytics />
|
||||
</NextAuthSessionProvider>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export function LoginForm({ onSwitchToRegister }: LoginFormProps) {
|
|||
setError("")
|
||||
setIsLoading(true)
|
||||
|
||||
// let's hope they know their password lmao
|
||||
const success = await login(email, password)
|
||||
if (!success) {
|
||||
setError("Ungültige E-Mail oder Passwort")
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export function RegisterForm({ onSwitchToLogin }: RegisterFormProps) {
|
|||
|
||||
setIsLoading(true)
|
||||
|
||||
// new user alert! make sure they don't break anything
|
||||
const success = await register(
|
||||
{
|
||||
firstname,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
DropdownMenuCheckboxItem,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { LogOut, User, Building2, Cpu } from "lucide-react"
|
||||
import { ModeToggle } from "@/components/mode-toggle"
|
||||
|
||||
interface AppShellProps {
|
||||
children: ReactNode
|
||||
|
|
@ -89,13 +90,13 @@ export function AppShell({ children }: AppShellProps) {
|
|||
<DropdownMenuSubContent className="max-h-60 overflow-y-auto">
|
||||
<DropdownMenuLabel>Räume verwalten</DropdownMenuLabel>
|
||||
{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 (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={room.id}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(checked) => {
|
||||
const currentIds = user.supervisedRooms.map((r) => r.id)
|
||||
const currentIds = user.supervisedRooms?.map((r) => r.id) || []
|
||||
let newIds
|
||||
if (checked) {
|
||||
newIds = [...currentIds, room.id]
|
||||
|
|
@ -120,6 +121,9 @@ export function AppShell({ children }: AppShellProps) {
|
|||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* dark mode toggle because staring at white screens hurts */}
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</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