Initial commit: LMS microservices scaffold

- Gradle multi-module: libs/{common-http,security-jwt,markdown,test-support},
  services/{identity,academic,progress,gateway}, web (Thymeleaf+htmx)
- identity-service: JWT auth (RSA), accounts, students, groups, enrollment,
  import from XLSX/CSV, credentials export, password reset
- academic-service: lab import from JSON/zip (markdown + YAML + variants),
  levels, API
- progress-service: schema for student levels and variant assignments
- gateway: Spring Cloud Gateway, JWT validation, route propagation
- web: Server-side UI with flex/footer layout, light/dark theme toggle,
  admin dashboard, groups CRUD, student portal placeholder
- docker-compose: PostgreSQL 18
- Tests: identity smoke test with Testcontainers
This commit is contained in:
2026-09-10 19:46:13 +03:00
commit 9b7748571e
119 changed files with 4671 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:fragment="layout(title, content)">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title th:replace="${title}">LMS</title>
<link rel="stylesheet" th:href="@{/css/app.css}">
<script th:src="@{/js/htmx.min.js}" defer></script>
<script>
(function(){
var t = localStorage.getItem('lms-theme') || 'light';
document.documentElement.setAttribute('data-theme', t);
})();
function toggleTheme() {
var e = document.documentElement;
var t = e.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
e.setAttribute('data-theme', t);
localStorage.setItem('lms-theme', t);
}
</script>
</head>
<body class="flex flex-col" style="min-height:100vh">
<header style="position:sticky;top:0;z-index:10;border-bottom:1px solid var(--c-border);background:var(--c-surface);">
<div style="max-width:1152px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;padding:0.75rem 1.5rem;">
<a th:href="@{/}" style="font-size:1.125rem;font-weight:600;text-decoration:none;">LMS</a>
<nav style="display:flex;align-items:center;gap:1rem;font-size:0.875rem;" th:if="${currentUser != null}">
<a th:if="${currentUser.admin}" th:href="@{/admin}">Панель</a>
<a th:if="${currentUser.admin}" th:href="@{/admin/groups}">Группы</a>
<span style="color:var(--c-text-muted)" th:text="${currentUser.name}">—</span>
<button onclick="toggleTheme()" class="btn btn-secondary" style="padding:0.25rem 0.5rem;font-size:0.75rem;">☀/🌙</button>
<form th:action="@{/logout}" method="post">
<button class="btn btn-secondary" type="submit">Выйти</button>
</form>
</nav>
</div>
</header>
<main style="flex:1;width:100%;max-width:1152px;margin:0 auto;padding:2rem 1.5rem;">
<th:block th:replace="${content}" />
</main>
<footer style="border-top:1px solid var(--c-border);padding:1rem;text-align:center;font-size:0.75rem;color:var(--c-text-muted);">
LMS v0.1.1
</footer>
</body>
</html>