diff --git a/auth/middleware.go b/auth/middleware.go index c1af8d6..8416191 100644 --- a/auth/middleware.go +++ b/auth/middleware.go @@ -13,24 +13,47 @@ const UserContextKey contextKey = contextKey("user") func AuthMiddleware(secret []byte) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - + tokenStr := "" authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Missing token", http.StatusUnauthorized) - return + + if strings.HasPrefix(authHeader, "Bearer ") { + tokenStr = strings.TrimPrefix(authHeader, "Bearer ") } - tokenStr := strings.TrimPrefix(authHeader, "Bearer ") + if tokenStr == "" { + cookie, err := r.Cookie("access_token") + if err == nil { + tokenStr = cookie.Value + } + } + + if tokenStr == "" { + if strings.HasPrefix(r.URL.Path, "/api/") { + http.Error(w, "Missing token", http.StatusUnauthorized) + } else { + http.Redirect(w, r, "/login", http.StatusSeeOther) + } + return + } claims, err := ValidateJWT(tokenStr, secret) + if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) + if strings.HasPrefix(r.URL.Path, "/api/") { + http.Error(w, "Invalid token", http.StatusUnauthorized) + } else { + http.Redirect(w, r, "/login", http.StatusSeeOther) + } return } - ctx := context.WithValue(r.Context(), UserContextKey, claims) - next.ServeHTTP(w, r.WithContext(ctx)) + ctx := context.WithValue( + r.Context(), + UserContextKey, + claims, + ) + next.ServeHTTP(w, r.WithContext(ctx)) }) } } diff --git a/frontend/assets/css/dashboard.css b/frontend/assets/css/dashboard.css index c565dc3..d1e3625 100644 --- a/frontend/assets/css/dashboard.css +++ b/frontend/assets/css/dashboard.css @@ -1,110 +1,302 @@ -:root { - --bg: #111827; - --card: #1f2937; - --border: #374151; - --text: #f9fafb; - --accent: #3b82f6; +main { + max-width: 1400px; + width: 100%; + margin: 0 auto; + padding: 2rem; + flex: 1; } -* { - box-sizing: border-box; +h1 { + font-size: 2rem; + font-weight: 700; + letter-spacing: -0.03em; + margin-bottom: 2rem; } -body { - margin: 0; - background: var(--bg); - color: var(--text); - font-family: system-ui; +/* Navigation Header */ +header { + width: 100%; + border-bottom: 1px solid var(--border); + background: rgba(31, 41, 55, 0.6); + backdrop-filter: blur(12px); + position: sticky; + top: 0; + z-index: 50; +} + +.nav-container { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem; + height: 4rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.nav-left { + display: flex; + align-items: center; + gap: 3rem; + height: 100%; +} + +.brand { + font-weight: 800; + font-size: 1.2rem; + letter-spacing: -0.03em; +} + +.brand span { + color: var(--accent); } nav { display: flex; - gap: 1rem; - - padding: 1rem; - - border-bottom: 1px solid var(--border); + gap: 1.5rem; + height: 100%; } nav a { - color: white; + color: var(--text-muted); text-decoration: none; + font-size: 0.95rem; + font-weight: 500; + display: inline-flex; + align-items: center; + border-bottom: 2px solid transparent; + padding: 0 0.25rem; + transition: color 0.15s ease, border-color 0.15s ease; } -main { - max-width: 1400px; - - margin: auto; - - padding: 2rem; +nav a:hover, nav a.active { + color: var(--text); + border-color: var(--accent); } -.cards { - display: grid; - - grid-template-columns: - repeat(auto-fit,minmax(250px,1fr)); - - gap: 1rem; +/* Profile Dropdown Component */ +.profile-dropdown { + position: relative; + display: inline-block; + height: 100%; + display: flex; + align-items: center; } -.card { - background: var(--card); +.profile-trigger { + background: none; + border: none; + display: flex; + align-items: center; + gap: 0.75rem; + color: var(--text); + cursor: pointer; + padding: 0.5rem; + border-radius: 8px; + transition: background-color 0.2s ease; +} +.profile-trigger:hover { + background-color: rgba(255, 255, 255, 0.05); +} + +.avatar { + width: 2rem; + height: 2rem; + background-color: var(--accent); + color: #ffffff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.9rem; +} + +.username { + font-size: 0.95rem; + font-weight: 500; +} + +.dropdown-menu { + position: absolute; + top: calc(100% - 0.5rem); + right: 0; + width: 200px; + background: #1f2937; border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); + display: none; + flex-direction: column; + padding: 0.5rem 0; + z-index: 100; +} +/* Show menu on hover OR when active via click class */ +.profile-dropdown:hover .dropdown-menu, +.dropdown-menu.show { + display: flex; +} + +.dropdown-menu a, .logout-btn { + width: 100%; + padding: 0.6rem 1.2rem; + font-size: 0.9rem; + color: var(--text-muted); + text-decoration: none; + text-align: left; + background: none; + border: none; + cursor: pointer; + transition: background-color 0.15s ease, color 0.15s ease; + box-sizing: border-box; +} + +.dropdown-menu a:hover { + background-color: rgba(255, 255, 255, 0.05); + color: var(--text); +} + +.dropdown-divider { + border: 0; + border-top: 1px solid var(--border); + margin: 0.4rem 0; +} + +.logout-btn { + color: #ef4444; + font-weight: 500; +} + +.logout-btn:hover { + background-color: rgba(239, 68, 68, 0.1); + color: #f87171; +} + +/* Stats Cards Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: var(--card); + border: 1px solid var(--border); border-radius: 12px; + padding: 1.5rem; + text-align: left; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); +} - padding: 1rem; +.stat-card h2 { + font-size: 2.25rem; + font-weight: 700; + letter-spacing: -0.03em; + margin-bottom: 0.25rem; + color: var(--text); +} + +.stat-card p { + color: var(--text-muted); + font-size: 0.9rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* Controls & Search */ +.action-bar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.search-input { + max-width: 320px; +} + +/* Data Tables */ +.table-container { + width: 100%; + background: var(--card); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } table { width: 100%; border-collapse: collapse; -} - -th, -td { - padding: 1rem; text-align: left; + font-size: 0.95rem; } -tr { +th { + background: #111827; + color: var(--text-muted); + font-weight: 600; + padding: 1rem 1.5rem; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.05em; border-bottom: 1px solid var(--border); } -input, -select, -button { - - background: #1f2937; - - color: white; - - border: 1px solid var(--border); - - border-radius: 8px; - - padding: .8rem; +td { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); + color: var(--text); } -button { - cursor: pointer; +tr:last-child td { + border-bottom: none; } -button:hover { - background: var(--accent); +tr:hover td { + background: rgba(255, 255, 255, 0.01); } -@media(max-width:768px){ +/* Modals & Placeholders */ +#modal { + position: fixed; + z-index: 100; +} - nav{ - flex-wrap:wrap; +@media (max-width: 768px) { + .nav-container { + flex-direction: column; + height: auto; + padding: 1rem; + gap: 1rem; } - - table{ - display:block; - overflow:auto; + .nav-left { + flex-direction: column; + gap: 1rem; + } + nav { + gap: 1rem; + } + .profile-dropdown { + width: 100%; + justify-content: center; + } + .dropdown-menu { + position: static; + width: 100%; + box-shadow: none; + margin-top: 0.5rem; + } + .action-bar { + flex-direction: column; + align-items: stretch; + } + .search-input { + max-width: 100%; } - } \ No newline at end of file diff --git a/frontend/assets/css/error404.css b/frontend/assets/css/error404.css new file mode 100644 index 0000000..d83333d --- /dev/null +++ b/frontend/assets/css/error404.css @@ -0,0 +1,14 @@ +/* error404.css */ +.error-code { + font-size: 6rem; + font-weight: 800; + color: var(--accent); + margin: 0; + line-height: 1; +} + +@media(max-width:768px){ + .error-code { + font-size: 4.5rem; + } +} \ No newline at end of file diff --git a/frontend/assets/css/home.css b/frontend/assets/css/home.css new file mode 100644 index 0000000..d4db74b --- /dev/null +++ b/frontend/assets/css/home.css @@ -0,0 +1,16 @@ +/* home.css */ +.brand-title { + font-size: 2.25rem !important; + font-weight: 800 !important; + letter-spacing: -0.04em !important; +} + +.brand-title span { + color: var(--accent); +} + +.home-actions { + display: flex; + flex-direction: column; + gap: 0.85rem; +} \ No newline at end of file diff --git a/frontend/assets/css/theme.css b/frontend/assets/css/theme.css new file mode 100644 index 0000000..064da8c --- /dev/null +++ b/frontend/assets/css/theme.css @@ -0,0 +1,171 @@ +/* theme.css */ +:root { + --bg: #111827; + --card: #1f2937; + --border: #374151; + --text: #f9fafb; + --text-muted: #9ca3af; + --accent: #3b82f6; + --accent-hover: #2563eb; + --success: #10b981; + --error: #ef4444; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--bg); + color: var(--text); + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 1rem; +} + +/* Gemeinsames Card-Layout für Home, Login, Register und 404 */ +.card { + width: 100%; + max-width: 400px; + background: var(--card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 2.5rem 2rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.3); + text-align: center; +} + +.card h1 { + margin: 0 0 0.5rem 0; + font-size: 1.75rem; + font-weight: 700; + letter-spacing: -0.025em; +} + +.card .subtitle { + color: var(--text-muted); + font-size: 0.95rem; + margin-bottom: 2rem; +} + +/* Button & Link Standard-Skins */ +.btn { + display: inline-flex; + justify-content: center; + align-items: center; + width: 100%; + padding: 0.85rem; + font-size: 1rem; + font-weight: 600; + border-radius: 10px; + border: none; + cursor: pointer; + text-decoration: none; + transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.1s ease; +} + +.btn:active { + transform: scale(0.98); +} + +.btn-primary { + background: var(--accent); + color: white; +} + +.btn-primary:hover { + background: var(--accent-hover); +} + +.btn-secondary { + background: #1f2937; + color: white; + border: 1px solid var(--border); +} + +.btn-secondary:hover { + background: var(--accent); + border-color: var(--accent); +} + +/* Formular-Elemente */ +.form-group { + margin-bottom: 1.25rem; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +input { + width: 100%; + background: #111827; + color: white; + border: 1px solid var(--border); + border-radius: 10px; + padding: 0.85rem 1rem; + font-size: 1rem; + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15); +} + +input::placeholder { + color: #4b5563; +} + +.footer-text { + margin-top: 1.5rem; + font-size: 0.9rem; + color: var(--text-muted); +} + +.footer-text a { + color: var(--accent); + text-decoration: none; +} + +.footer-text a:hover { + text-decoration: underline; +} + +/* Feedback-Boxen */ +.message { + display: none; + margin-top: 1.25rem; + padding: 0.85rem 1rem; + border-radius: 10px; + font-size: 0.9rem; + line-height: 1.4; + text-align: left; +} + +.message.error { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.2); + color: var(--error); +} + +.message.success { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + color: var(--success); +} \ No newline at end of file diff --git a/frontend/assets/js/auth.js b/frontend/assets/js/auth.js new file mode 100644 index 0000000..5f43a5c --- /dev/null +++ b/frontend/assets/js/auth.js @@ -0,0 +1,109 @@ +// auth.js +(() => { + const currentPath = window.location.pathname; + + if (currentPath !== "/" && currentPath !== "/login" && currentPath !== "/register") { + return; + } + + function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return null; + } + + const cookieAccessToken = getCookie("access_token"); + const cookieRefreshToken = getCookie("refresh_token"); + + const localAccessToken = localStorage.getItem("access_token"); + const localRefreshToken = localStorage.getItem("refresh_token"); + + const accessToken = cookieAccessToken || localAccessToken; + const refreshToken = cookieRefreshToken || localRefreshToken; + + if (!accessToken && !refreshToken) { + return; + } + + async function tryTokenRefresh() { + if (!refreshToken) return false; + + try { + const response = await fetch("/api/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: refreshToken }) + }); + + if (response.ok) { + const data = await response.json(); + + localStorage.setItem("access_token", data.access_token); + localStorage.setItem("refresh_token", data.refresh_token); + + document.cookie = `access_token=${data.access_token}; path=/; max-age=900; SameSite=Lax; Secure`; + document.cookie = `refresh_token=${data.refresh_token}; path=/; max-age=604800; SameSite=Lax; Secure`; + + return true; + } + } catch (err) { + console.error("Refresh request failed:", err); + } + + document.cookie = "access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;"; + document.cookie = "refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;"; + return false; + } + + async function checkAuth() { + console.log("Auth check started..."); + console.log("AccessToken present:", !!accessToken); + console.log("RefreshToken present:", !!refreshToken); + + if (!cookieAccessToken && accessToken) { + console.log("Access token cookie missing, but present in localStorage. Forcing refresh..."); + } else if (accessToken) { + try { + console.log("Attempting ping with access token..."); + const response = await fetch("/api/ping", { + method: "GET", + headers: { "Authorization": `Bearer ${accessToken}` } + }); + + if (response.ok) { + console.log("Ping successful! Redirecting to dashboard..."); + window.location.href = "/dashboard"; + return; + } else { + console.log("Ping failed. Status:", response.status); + } + } catch (err) { + console.error("Network error during ping:", err); + } + } + + if (refreshToken) { + console.log("Starting token refresh to rebuild cookies..."); + const refreshSuccessful = await tryTokenRefresh(); + + if (refreshSuccessful) { + console.log("Refresh successful! Redirecting to dashboard..."); + window.location.href = "/dashboard"; + return; + } else { + console.log("Refresh failed. Staying on login."); + } + } else { + console.log("No refresh token present. User must log in normally."); + } + + console.log("Authentication completely failed. Clearing remnants..."); + localStorage.removeItem("access_token"); + localStorage.removeItem("refresh_token"); + document.cookie = "access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;"; + document.cookie = "refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC;"; + } + + checkAuth(); +})(); \ No newline at end of file diff --git a/frontend/assets/js/login.js b/frontend/assets/js/login.js new file mode 100644 index 0000000..717eabe --- /dev/null +++ b/frontend/assets/js/login.js @@ -0,0 +1,42 @@ +// login.js +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById("login-form"); + const errorBox = document.getElementById("error"); + + if (!form) return; + + form.addEventListener("submit", async (e) => { + e.preventDefault(); + errorBox.style.display = "none"; + + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + + try { + const response = await fetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }) + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(text); + } + + const data = await response.json(); + + localStorage.setItem("access_token", data.access_token); + localStorage.setItem("refresh_token", data.refresh_token); + + document.cookie = `access_token=${data.access_token}; path=/; max-age=900; SameSite=Lax; Secure`; + document.cookie = `refresh_token=${data.refresh_token}; path=/; max-age=604800; SameSite=Lax; Secure`; + + window.location.href = "/dashboard"; + + } catch (err) { + errorBox.textContent = err.message || "Login failed."; + errorBox.style.display = "block"; + } + }); +}); \ No newline at end of file diff --git a/frontend/assets/js/register.js b/frontend/assets/js/register.js new file mode 100644 index 0000000..e97f475 --- /dev/null +++ b/frontend/assets/js/register.js @@ -0,0 +1,43 @@ +// register.js +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById("register-form"); + const msgBox = document.getElementById("message"); + + form.addEventListener("submit", async (e) => { + e.preventDefault(); + + msgBox.style.display = "none"; + msgBox.className = "message"; + + const username = document.getElementById("username").value; + const password = document.getElementById("password").value; + + try { + const response = await fetch("/api/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }) + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(text); + } + + msgBox.textContent = "Registration successful! Redirecting..."; + msgBox.classList.add("success"); + msgBox.style.display = "block"; + + form.querySelector("button").disabled = true; + + setTimeout(() => { + window.location.href = "/login"; + }, 1500); + + } catch (err) { + msgBox.textContent = err.message; + msgBox.classList.add("error"); + msgBox.style.display = "block"; + } + }); +}); \ No newline at end of file diff --git a/frontend/handler.go b/frontend/handler.go index 31043ae..249d0b0 100644 --- a/frontend/handler.go +++ b/frontend/handler.go @@ -7,12 +7,34 @@ import ( "strings" ) -var dashbaord = template.Must(template.ParseFiles( +var dashboard = template.Must(template.ParseFiles( "frontend/htmx/contents/dash/base.html", "frontend/htmx/contents/dash/dashboard.html")) + +var inventory = template.Must(template.ParseFiles( + "frontend/htmx/contents/dash/base.html", + "frontend/htmx/contents/dash/inventory.html")) + +var item = template.Must(template.ParseFiles( + "frontend/htmx/contents/dash/base.html", + "frontend/htmx/contents/dash/itemlist.html")) + +var locations = template.Must(template.ParseFiles( + "frontend/htmx/contents/dash/base.html", + "frontend/htmx/contents/dash/locations.html")) + +var projects = template.Must(template.ParseFiles( + "frontend/htmx/contents/dash/base.html", + "frontend/htmx/contents/dash/projects.html")) + var home = template.Must(template.ParseFiles("frontend/htmx/home.html")) func Home(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.ServeFile(w, r, "frontend/htmx/404.html") + return + } + w.Header().Set("Content-Type", "text/html") err := home.Execute(w, struct { Name string @@ -26,7 +48,51 @@ func Home(w http.ResponseWriter, r *http.Request) { func Dashboard(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") - err := dashbaord.Execute(w, struct { + err := dashboard.ExecuteTemplate(w, "base.html", struct { + Title string + }{ + Title: "Miau", + }) + if err != nil { + return + } +} +func Inventory(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + err := inventory.ExecuteTemplate(w, "base.html", struct { + Title string + }{ + Title: "Miau", + }) + if err != nil { + return + } +} +func Items(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + err := item.ExecuteTemplate(w, "base.html", struct { + Title string + }{ + Title: "Miau", + }) + if err != nil { + return + } +} +func Locations(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + err := locations.ExecuteTemplate(w, "base.html", struct { + Title string + }{ + Title: "Miau", + }) + if err != nil { + return + } +} +func Projects(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + err := projects.ExecuteTemplate(w, "base.html", struct { Title string }{ Title: "Miau", diff --git a/frontend/htmx/404.html b/frontend/htmx/404.html new file mode 100644 index 0000000..5507027 --- /dev/null +++ b/frontend/htmx/404.html @@ -0,0 +1,23 @@ + + + + + + 404 - Page Not Found | MiauInv + + + + + +
+
404
+

Page Not Found

+

The page you are looking for does not exist or has been moved to another address.

+ + + Back to Dashboard + +
+ + + \ No newline at end of file diff --git a/frontend/htmx/contents/dash/base.html b/frontend/htmx/contents/dash/base.html index 9da79d1..4130054 100644 --- a/frontend/htmx/contents/dash/base.html +++ b/frontend/htmx/contents/dash/base.html @@ -1,27 +1,82 @@ - - +{{ define "base.html" }} + - {{ .Title }} - + {{ .Title }} | MiauInv - + - - +
+ +
{{ template "content" . }}
+ - \ No newline at end of file + +{{ end }} \ No newline at end of file diff --git a/frontend/htmx/contents/dash/dashboard.html b/frontend/htmx/contents/dash/dashboard.html index 803c902..cefd2d1 100644 --- a/frontend/htmx/contents/dash/dashboard.html +++ b/frontend/htmx/contents/dash/dashboard.html @@ -1,24 +1,20 @@ {{ define "content" }} -

Dashboard

-
- -
+
+

{{ .Stats.Items }}

Items

-
+

{{ .Stats.Projects }}

-

Projekte

+

Projects

-
+

{{ .Stats.Locations }}

-

Orte

+

Locations

-
- {{ end }} \ No newline at end of file diff --git a/frontend/htmx/contents/dash/inventory.html b/frontend/htmx/contents/dash/inventory.html index 38f265a..e731d1d 100644 --- a/frontend/htmx/contents/dash/inventory.html +++ b/frontend/htmx/contents/dash/inventory.html @@ -1,21 +1,24 @@ {{ define "content" }} +

Inventory

-

Inventar

- - - -
+
+
+
+
+
+
{{ end }} \ No newline at end of file diff --git a/frontend/htmx/contents/dash/itemlist.html b/frontend/htmx/contents/dash/itemlist.html index 1516712..a612e05 100644 --- a/frontend/htmx/contents/dash/itemlist.html +++ b/frontend/htmx/contents/dash/itemlist.html @@ -1,32 +1,20 @@ - - - - + + + - - {{ range . }} - - - - - - - - - - + + + + - {{ end }} - -
NameKategorieGesamtFreiCategoryTotalFree
{{ .Name }}{{ .Category }}{{ .TotalQuantity }}{{ .FreeQuantity }}{{ .Name }}{{ .Category }}{{ .TotalQuantity }}{{ .FreeQuantity }}
\ No newline at end of file diff --git a/frontend/htmx/contents/dash/locations.html b/frontend/htmx/contents/dash/locations.html index 731a1a2..0d69b14 100644 --- a/frontend/htmx/contents/dash/locations.html +++ b/frontend/htmx/contents/dash/locations.html @@ -1,11 +1,11 @@ {{ define "content" }} +

Locations

-

Lagerorte

- -
+
+
+
- {{ end }} \ No newline at end of file diff --git a/frontend/htmx/contents/dash/projects.html b/frontend/htmx/contents/dash/projects.html index cdd4ee5..efff1a3 100644 --- a/frontend/htmx/contents/dash/projects.html +++ b/frontend/htmx/contents/dash/projects.html @@ -1,21 +1,22 @@ {{ define "content" }} - -

Projekte

- - +
+

Projects

+ +
- {{ end }} \ No newline at end of file diff --git a/frontend/htmx/home.html b/frontend/htmx/home.html index 44e22ab..2b1767d 100644 --- a/frontend/htmx/home.html +++ b/frontend/htmx/home.html @@ -1,6 +1,26 @@ - - -

Hallo, {{.Name}}!

- + + + + + MiauInv | Private Instance + + + + + + +
+
+

MiauInv

+

Private Instance

+
+ + +
+ + \ No newline at end of file diff --git a/frontend/htmx/login.html b/frontend/htmx/login.html new file mode 100644 index 0000000..17527a8 --- /dev/null +++ b/frontend/htmx/login.html @@ -0,0 +1,43 @@ + + + + + + Sign In | MiauInv + + + + + + + + +
+
+

Welcome back

+

Sign in to your account

+
+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+ + +
+ + + \ No newline at end of file diff --git a/frontend/htmx/register.html b/frontend/htmx/register.html new file mode 100644 index 0000000..d7f8742 --- /dev/null +++ b/frontend/htmx/register.html @@ -0,0 +1,41 @@ + + + + + + Register | MiauInv + + + + + +
+
+

Create Account

+

Register to continue

+
+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/handlers/account.go b/handlers/account.go index 7c8829a..6725c00 100644 --- a/handlers/account.go +++ b/handlers/account.go @@ -15,7 +15,7 @@ import ( var cfg, _ = config.LoadConfig() -func Register(w http.ResponseWriter, r *http.Request) { +func APIRegister(w http.ResponseWriter, r *http.Request) { var user models.User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { log.Println("POST [api/register] " + r.RemoteAddr + ": " + err.Error()) @@ -48,7 +48,7 @@ func Register(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) log.Println("POST [api/register] " + r.RemoteAddr + ": Successfully created user") } -func Login(w http.ResponseWriter, r *http.Request) { +func APILogin(w http.ResponseWriter, r *http.Request) { var creds struct { Username string `json:"username"` Password string `json:"password"` @@ -123,6 +123,23 @@ func Login(w http.ResponseWriter, r *http.Request) { }, } + http.SetCookie(w, &http.Cookie{ + Name: "access_token", + Value: accessToken, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + http.SetCookie(w, &http.Cookie{ + Name: "refresh_token", + Value: refreshTokenPlain, + Path: "/", + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteLaxMode, + }) + w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(resp) if err != nil { @@ -155,7 +172,7 @@ func TestHandler(w http.ResponseWriter, r *http.Request) { log.Println("GET [api/ping] " + r.RemoteAddr + ": " + err.Error()) return } - log.Println("GET [api/login] " + r.RemoteAddr + ": Successfully tested connection") + log.Println("GET [api/ping] " + r.RemoteAddr + ": Successfully tested connection") } func RefreshToken(w http.ResponseWriter, r *http.Request) { var req struct { diff --git a/server/server.go b/server/server.go index 074df20..c63acfa 100644 --- a/server/server.go +++ b/server/server.go @@ -1,9 +1,11 @@ package server import ( + "MiauInv/auth" "MiauInv/config" "MiauInv/frontend" "MiauInv/handlers" + utils "MiauInv/util" "log" "net/http" "os" @@ -58,32 +60,32 @@ func (this *Server) Run() { // FRONTEND // mux.HandleFunc("/", frontend.Home) - mux.HandleFunc("/dashboard", frontend.Dashboard) + mux.HandleFunc("/login", utils.RenderFile("frontend/htmx/login.html")) + mux.HandleFunc("/register", utils.RenderFile("frontend/htmx/register.html")) + + mux.Handle("/dashboard", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Dashboard))) + mux.Handle("/inventory", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Inventory))) + mux.Handle("/items", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Items))) + mux.Handle("/locations", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Locations))) + mux.Handle("/projects", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(frontend.Projects))) // // API // - - // Public - mux.HandleFunc("/api/login", handlers.Login) - mux.HandleFunc("/api/register", handlers.Register) + mux.HandleFunc("/api/login", handlers.APILogin) + mux.HandleFunc("/api/register", handlers.APIRegister) mux.HandleFunc("/api/refresh", handlers.RefreshToken) - mux.HandleFunc("/api/logout", handlers.Logout) + mux.Handle("/api/logout", auth.AuthMiddleware(this.JWTSecret)(http.HandlerFunc(handlers.Logout))) - mux.HandleFunc("/api/items", handlers.GetItems) - mux.HandleFunc("/api/items/create", handlers.CreateItem) - mux.HandleFunc("/api/locations/create", handlers.CreateLocation) - mux.HandleFunc("/api/projects/create", handlers.CreateProject) + mux.HandleFunc("/api/item", handlers.GetItems) + mux.HandleFunc("/api/locations", handlers.CreateLocation) + mux.HandleFunc("/api/project", handlers.CreateProject) mux.HandleFunc("/api/stock/add", handlers.AddStock) mux.HandleFunc("/api/project-items/add", handlers.AllocateToProject) // Assets mux.HandleFunc("/assets/", frontend.Assets) - // Login required - - // Admin-only - log.Printf("Listening on port %s", this.Port) log.Fatal(http.ListenAndServeTLS(":"+this.Port, this.CertificatePath, this.PrivateKeyPath, mux)) } diff --git a/util/util.go b/util/util.go index e75a4db..5a119b5 100644 --- a/util/util.go +++ b/util/util.go @@ -47,3 +47,8 @@ func IsLoggedIn(w http.ResponseWriter, r *http.Request) (*auth.Claims, bool) { } return claims, true } +func RenderFile(path string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, path) + } +}