feat(account): add activation and authentication web flow

This commit is contained in:
sechmachine
2026-08-15 00:45:34 +07:00
parent 98a52a1ac2
commit 8e786ba37b
7 changed files with 397 additions and 3 deletions
@@ -0,0 +1,85 @@
package com.lab.labtimesheet.feature.account.controller;
import java.security.Principal;
import java.time.LocalDate;
import com.lab.labtimesheet.feature.account.model.GlobalRole;
import com.lab.labtimesheet.feature.account.model.dto.CreateAccountCommand;
import com.lab.labtimesheet.feature.account.service.AccountService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
class AccountController {
private final AccountService accounts;
AccountController(AccountService accounts) {
this.accounts = accounts;
}
@GetMapping("/admin/accounts/new")
String newAccount() {
return "accounts/new";
}
@PostMapping("/admin/accounts")
String create(
@RequestParam String email,
@RequestParam String displayName,
@RequestParam GlobalRole role,
@RequestParam(required = false) String studentCode,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate internshipStart,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate internshipEnd,
Principal principal,
Model model) {
try {
var result = accounts.create(
new CreateAccountCommand(
email, displayName, role, clean(studentCode), internshipStart, internshipEnd),
accounts.requireActiveAdminId(principal.getName()));
return result.deliverySucceeded()
? "redirect:/admin/accounts/new?created"
: "redirect:/admin/accounts/new?deliveryFailed";
} catch (IllegalArgumentException | IllegalStateException exception) {
model.addAttribute("error", exception.getMessage());
return "accounts/new";
}
}
private static String clean(String value) {
return value == null || value.isBlank() ? null : value.trim();
}
@GetMapping("/activate")
String activationForm(@RequestParam String token, Model model) {
model.addAttribute("token", token);
return "accounts/activate";
}
@PostMapping("/activate")
String activate(
@RequestParam String token,
@RequestParam String password,
@RequestParam String confirmPassword,
Model model) {
if (!password.equals(confirmPassword)) {
model.addAttribute("token", token);
model.addAttribute("error", "Passwords do not match");
return "accounts/activate";
}
try {
if (accounts.activate(token, password)) {
return "redirect:/login?activated";
}
model.addAttribute("error", "This activation link is invalid or no longer usable");
} catch (IllegalArgumentException exception) {
model.addAttribute("error", exception.getMessage());
}
model.addAttribute("token", token);
return "accounts/activate";
}
}
@@ -0,0 +1,16 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><title>Activate account</title></head>
<body>
<main>
<h1>Choose your password</h1>
<p th:if="${error}" th:text="${error}" role="alert"></p>
<form method="post" th:action="@{/activate}">
<input name="token" type="hidden" th:value="${token}">
<label>Password <input name="password" type="password" minlength="12" maxlength="128" required autocomplete="new-password"></label>
<label>Confirm password <input name="confirmPassword" type="password" minlength="12" maxlength="128" required autocomplete="new-password"></label>
<button type="submit">Activate account</button>
</form>
</main>
</body>
</html>
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><title>Create account</title></head>
<body>
<main>
<h1>Create account</h1>
<p th:if="${param.created}" role="status">Account created and activation email sent.</p>
<p th:if="${param.deliveryFailed}" role="alert">Account created, but activation delivery failed.</p>
<p th:if="${error}" th:text="${error}" role="alert"></p>
<form method="post" th:action="@{/admin/accounts}">
<label>Email <input name="email" type="email" required autocomplete="off"></label>
<label>Display name <input name="displayName" required autocomplete="off"></label>
<label>Role
<select name="role" required>
<option value="ADMIN">Admin</option>
<option value="MENTOR">Mentor</option>
<option value="INTERN">Intern</option>
</select>
</label>
<fieldset>
<legend>Intern details</legend>
<label>Student code <input name="studentCode" autocomplete="off"></label>
<label>Internship start <input name="internshipStart" type="date"></label>
<label>Internship end <input name="internshipEnd" type="date"></label>
</fieldset>
<button type="submit">Create account</button>
</form>
</main>
</body>
</html>
+7 -2
View File
@@ -1,5 +1,10 @@
<!doctype html>
<html lang="en">
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="utf-8"><title>Lab Timesheet</title></head>
<body><main><h1>Lab Timesheet</h1></main></body>
<body>
<main>
<h1>Lab Timesheet</h1>
<form method="post" th:action="@{/logout}"><button type="submit">Sign out</button></form>
</main>
</body>
</html>