Checkout:
- assets.AssignUser + POST /app/assets/{id}/checkout assign/unassign an
asset's user independent of the full edit form. New shared
#checkoutModal, wired from a "Check Out..." context-menu action, backed
by GET /app/assets/{id} and GET /app/users/options JSON endpoints.
Quantity / uniqueness:
- Item types gain an IsUnique flag (checked by default, matching prior
behavior). Assets gain a Quantity column, fixed at 1 for unique types
and adjustable via +/- steppers (POST /app/assets/{id}/quantity) for
non-unique ones. Flipping a type back to unique clamps existing assets'
quantity back to 1. CSV import/export gained a Quantity column.
Migration 0003_quantity.sql.
Contacts:
- New models.RoleContact: an assignable person who cannot sign in
(User.CanLogin). Migration 0004_contacts.sql rebuilds the users table
(SQLite can't alter a CHECK constraint in place) to allow the role.
Users form/list updated; contact creation auto-generates an unusable
password since the login path is blocked by role regardless.
QR asset tags and printable labels:
- Each asset's QR code (via github.com/skip2/go-qrcode, pure Go) encodes
an absolute /scan/<tag> URL; GET /scan/{tag} resolves it and redirects
into /inventory?asset=<id>, which app.js picks up on load to
auto-select that asset - works from a phone camera or a USB
scanner-as-keyboard-wedge pointed at the address bar.
- GET /labels renders a print-ready sheet (right-click an asset for one
label, a folder for every asset in it) with a template switcher between
Avery 5160 (verified dimensions) and 61526 asset tag labels (best-effort
layout, flagged as such on the page).
Also refreshed README to reflect the right-click-first tree UX and all of
the above.
121 lines
6.4 KiB
Go
121 lines
6.4 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
content "inventree/web"
|
|
)
|
|
|
|
// Router builds the complete HTTP handler for inventree: static assets,
|
|
// authentication, the AJAX-driven inventory tree, and the traditional
|
|
// full-page admin sections (item types, users, settings).
|
|
func (a *App) Router() http.Handler {
|
|
mux := http.NewServeMux()
|
|
|
|
// Static assets (Bootstrap, icons, vanilla JS) and health check are
|
|
// always public.
|
|
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServerFS(mustSub(content.StaticFS, "static"))))
|
|
mux.HandleFunc("GET /healthz", a.handleHealthz)
|
|
|
|
// Auth
|
|
mux.HandleFunc("GET /login", a.handleLoginForm)
|
|
mux.HandleFunc("POST /login", a.handleLoginSubmit)
|
|
mux.HandleFunc("POST /logout", a.handleLogout)
|
|
|
|
mux.HandleFunc("GET /{$}", a.requireLogin(a.handleRoot))
|
|
|
|
// Inventory tree (AJAX fragments)
|
|
mux.HandleFunc("GET /inventory", a.requireLogin(a.handleInventoryPage))
|
|
mux.HandleFunc("GET /app/tree/children", a.requireLogin(a.handleTreeChildren))
|
|
mux.HandleFunc("POST /app/tree/collapse", a.requireLogin(a.handleTreeCollapse))
|
|
mux.HandleFunc("POST /app/tree/collapse-all", a.requireLogin(a.handleTreeCollapseAll))
|
|
|
|
mux.HandleFunc("GET /app/panel/root", a.requireLogin(a.handlePanelRoot))
|
|
mux.HandleFunc("GET /app/panel/folders/{id}", a.requireLogin(a.handlePanelFolder))
|
|
mux.HandleFunc("GET /app/panel/assets/{id}", a.requireLogin(a.handlePanelAsset))
|
|
mux.HandleFunc("GET /app/panel/assets/new", a.requireAdmin(a.handlePanelAssetNew))
|
|
mux.HandleFunc("GET /app/panel/assets/{id}/edit", a.requireAdmin(a.handlePanelAssetEdit))
|
|
mux.HandleFunc("GET /app/forms/asset-fields", a.requireLogin(a.handleAssetFieldsFragment))
|
|
|
|
mux.HandleFunc("GET /app/search", a.requireLogin(a.handleSearch))
|
|
|
|
// Folder mutations (JSON in/out)
|
|
mux.HandleFunc("GET /app/folders/options", a.requireAdmin(a.handleFolderOptions))
|
|
mux.HandleFunc("GET /app/folders/{id}", a.requireAdmin(a.handleFolderGet))
|
|
mux.HandleFunc("POST /app/folders", a.requireAdmin(a.handleFolderCreate))
|
|
mux.HandleFunc("PATCH /app/folders/{id}", a.requireAdmin(a.handleFolderUpdate))
|
|
mux.HandleFunc("POST /app/folders/{id}/move", a.requireAdmin(a.handleFolderMove))
|
|
mux.HandleFunc("GET /app/folders/{id}/delete-info", a.requireAdmin(a.handleFolderDeleteInfo))
|
|
mux.HandleFunc("DELETE /app/folders/{id}", a.requireAdmin(a.handleFolderDelete))
|
|
|
|
// Asset mutations (JSON in/out)
|
|
mux.HandleFunc("GET /app/assets/{id}", a.requireAdmin(a.handleAssetGet))
|
|
mux.HandleFunc("POST /app/assets", a.requireAdmin(a.handleAssetCreate))
|
|
mux.HandleFunc("PATCH /app/assets/{id}", a.requireAdmin(a.handleAssetUpdate))
|
|
mux.HandleFunc("DELETE /app/assets/{id}", a.requireAdmin(a.handleAssetDelete))
|
|
mux.HandleFunc("POST /app/assets/{id}/duplicate", a.requireAdmin(a.handleAssetDuplicate))
|
|
mux.HandleFunc("POST /app/assets/{id}/move", a.requireAdmin(a.handleAssetMove))
|
|
mux.HandleFunc("POST /app/assets/{id}/checkout", a.requireAdmin(a.handleAssetCheckout))
|
|
mux.HandleFunc("POST /app/assets/{id}/quantity", a.requireAdmin(a.handleAssetQuantity))
|
|
|
|
mux.HandleFunc("GET /app/users/options", a.requireAdmin(a.handleUserOptions))
|
|
|
|
// CSV import/export
|
|
mux.HandleFunc("GET /export/assets.csv", a.requireLogin(a.handleExportCSV))
|
|
mux.HandleFunc("POST /import/assets.csv", a.requireAdmin(a.handleImportCSV))
|
|
|
|
// QR asset tags: scan a printed label to jump to that asset, or print
|
|
// a sheet of labels for a folder/asset.
|
|
mux.HandleFunc("GET /scan/{tag}", a.requireLogin(a.handleScan))
|
|
mux.HandleFunc("GET /labels", a.requireLogin(a.handleLabelsPage))
|
|
|
|
// Settings (admin, traditional pages) - Item Types, Users, and general
|
|
// settings all live under one unified /settings section with shared
|
|
// sub-navigation, rather than as separate top-level nav items.
|
|
mux.HandleFunc("GET /settings", a.requireAdmin(a.handleSettingsPage))
|
|
mux.HandleFunc("POST /settings/general", a.requireAdmin(a.handleSettingsGeneral))
|
|
mux.HandleFunc("GET /settings/statuses", a.requireAdmin(a.handleSettingsStatusesPage))
|
|
mux.HandleFunc("POST /settings/statuses", a.requireAdmin(a.handleStatusCreate))
|
|
mux.HandleFunc("POST /settings/statuses/{id}", a.requireAdmin(a.handleStatusUpdate))
|
|
mux.HandleFunc("POST /settings/statuses/{id}/delete", a.requireAdmin(a.handleStatusDelete))
|
|
mux.HandleFunc("POST /settings/statuses/{id}/reorder", a.requireAdmin(a.handleStatusReorder))
|
|
|
|
mux.HandleFunc("GET /settings/item-types", a.requireAdmin(a.handleItemTypesList))
|
|
mux.HandleFunc("GET /settings/item-types/new", a.requireAdmin(a.handleItemTypeNewForm))
|
|
mux.HandleFunc("POST /settings/item-types", a.requireAdmin(a.handleItemTypeCreate))
|
|
mux.HandleFunc("GET /settings/item-types/{id}/edit", a.requireAdmin(a.handleItemTypeEditForm))
|
|
mux.HandleFunc("POST /settings/item-types/{id}", a.requireAdmin(a.handleItemTypeUpdate))
|
|
mux.HandleFunc("POST /settings/item-types/{id}/delete", a.requireAdmin(a.handleItemTypeDelete))
|
|
mux.HandleFunc("POST /settings/item-types/{id}/fields", a.requireAdmin(a.handleFieldCreate))
|
|
mux.HandleFunc("POST /settings/item-types/{id}/fields/{fieldID}", a.requireAdmin(a.handleFieldUpdate))
|
|
mux.HandleFunc("POST /settings/item-types/{id}/fields/{fieldID}/delete", a.requireAdmin(a.handleFieldDelete))
|
|
|
|
mux.HandleFunc("GET /settings/import", a.requireAdmin(a.handleSettingsImportPage))
|
|
mux.HandleFunc("POST /settings/import/assets", a.requireAdmin(a.handleSettingsImportAssets))
|
|
mux.HandleFunc("POST /settings/import/statuses", a.requireAdmin(a.handleSettingsImportStatuses))
|
|
|
|
mux.HandleFunc("GET /settings/users", a.requireAdmin(a.handleUsersList))
|
|
mux.HandleFunc("GET /settings/users/new", a.requireAdmin(a.handleUserNewForm))
|
|
mux.HandleFunc("POST /settings/users", a.requireAdmin(a.handleUserCreate))
|
|
mux.HandleFunc("GET /settings/users/{id}/edit", a.requireAdmin(a.handleUserEditForm))
|
|
mux.HandleFunc("POST /settings/users/{id}", a.requireAdmin(a.handleUserUpdate))
|
|
mux.HandleFunc("POST /settings/users/{id}/password", a.requireAdmin(a.handleUserPassword))
|
|
mux.HandleFunc("POST /settings/users/{id}/delete", a.requireAdmin(a.handleUserDelete))
|
|
|
|
return a.loadSession(mux)
|
|
}
|
|
|
|
func (a *App) handleHealthz(w http.ResponseWriter, r *http.Request) {
|
|
if err := a.DB.PingContext(r.Context()); err != nil {
|
|
writeErr(w, http.StatusServiceUnavailable, "database unavailable")
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("ok"))
|
|
}
|
|
|
|
func (a *App) handleRoot(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/inventory", http.StatusSeeOther)
|
|
}
|