Initial implementation of inventree

A small IT asset inventory system built around a nested folder tree,
matching the project brief: Go + html/template + Bootstrap 5 + vanilla JS,
SQLite storage (WAL, FKs, migrations), embedded templates/static assets,
single compiled binary.

- AJAX-driven inventory tree: lazy-loaded folder expansion with per-user
  persisted expand state, drag-and-drop move for folders/assets, safe
  delete-with-relocate-or-cascade confirmation, search with breadcrumbs.
- Assets: CRUD, duplicate, move, auto-generated IT-000001-style IDs,
  CSV import (with optional folder auto-create) and export.
- Admin-defined item types with custom fields (text/long_text/number/date/
  boolean/dropdown/url/ip_address).
- Configurable statuses, session-based auth (bcrypt, secure cookies),
  admin/user roles, user management.
- CLI: serve, version, database migrate, database backup; /healthz;
  graceful shutdown; Dockerfile/docker-compose/systemd unit; Go test suite.
This commit is contained in:
Owen
2026-08-17 13:34:40 -05:00
commit 218687a1ee
71 changed files with 8102 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
package web
import (
"net/http"
"inventree/internal/auth"
)
type loginPageData struct {
Error string
Next string
}
func (a *App) handleLoginForm(w http.ResponseWriter, r *http.Request) {
if _, ok := auth.UserFromContext(r.Context()); ok {
http.Redirect(w, r, "/inventory", http.StatusSeeOther)
return
}
a.Render(w, http.StatusOK, "login", loginPageData{Next: r.URL.Query().Get("next")})
}
func (a *App) handleLoginSubmit(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
a.Render(w, http.StatusBadRequest, "login", loginPageData{Error: "Invalid form submission."})
return
}
username := r.FormValue("username")
password := r.FormValue("password")
next := r.FormValue("next")
u, err := a.Users.Authenticate(r.Context(), username, password)
if err != nil {
a.Render(w, http.StatusUnauthorized, "login", loginPageData{Error: "Invalid username or password.", Next: next})
return
}
token, expires, err := a.Auth.CreateSession(r.Context(), u.ID)
if err != nil {
a.Render(w, http.StatusInternalServerError, "login", loginPageData{Error: "Could not start a session. Please try again."})
return
}
auth.SetCookie(w, token, expires, a.SecureCookies)
dest := "/inventory"
if next != "" {
dest = next
}
http.Redirect(w, r, dest, http.StatusSeeOther)
}
func (a *App) handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(auth.CookieName); err == nil {
_ = a.Auth.DestroySession(r.Context(), cookie.Value)
}
auth.ClearCookie(w, a.SecureCookies)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}