Compare commits
8 Commits
v1.0.0
...
feature/5-
| Author | SHA1 | Date | |
|---|---|---|---|
|
5485fd135d
|
|||
|
5558d42bdb
|
|||
|
b74df36bda
|
|||
|
918b9a6b74
|
|||
|
6d32ca13ca
|
|||
|
feffff0898
|
|||
|
5089f94a21
|
|||
|
f5f5da51c8
|
@@ -17,5 +17,9 @@ RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
|
|||||||
go build -ldflags "-s -w" -o MiauInv .
|
go build -ldflags "-s -w" -o MiauInv .
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
|
||||||
COPY --from=builder /app/MiauInv /MiauInv
|
COPY --from=builder /app/MiauInv /MiauInv
|
||||||
|
|
||||||
|
COPY --from=builder /app/frontend /frontend
|
||||||
|
|
||||||
ENTRYPOINT ["/MiauInv"]
|
ENTRYPOINT ["/MiauInv"]
|
||||||
88
README.md
88
README.md
@@ -6,6 +6,7 @@ MiauInv is a secure, light-weight inventory, stock, and project allocation track
|
|||||||
|
|
||||||
* [Technical Specifications](#technical-specifications)
|
* [Technical Specifications](#technical-specifications)
|
||||||
* [Architecture Overview](#architecture-overview)
|
* [Architecture Overview](#architecture-overview)
|
||||||
|
* [Detailed Documentation](#detailed-documentation)
|
||||||
* [Configuration](#configuration)
|
* [Configuration](#configuration)
|
||||||
* [Configuration File (config.yaml)](#configuration-file-configyaml)
|
* [Configuration File (config.yaml)](#configuration-file-configyaml)
|
||||||
* [Environment Variables](#environment-variables)
|
* [Environment Variables](#environment-variables)
|
||||||
@@ -16,6 +17,8 @@ MiauInv is a secure, light-weight inventory, stock, and project allocation track
|
|||||||
* [Prerequisites](#prerequisites)
|
* [Prerequisites](#prerequisites)
|
||||||
* [Option 1: Native Local Deployment](#option-1-native-local-deployment)
|
* [Option 1: Native Local Deployment](#option-1-native-local-deployment)
|
||||||
* [Option 2: Docker Deployment (Recommended)](#option-2-docker-deployment-recommended)
|
* [Option 2: Docker Deployment (Recommended)](#option-2-docker-deployment-recommended)
|
||||||
|
* [Reverse Proxy Integration with Caddy](#reverse-proxy-integration-with-caddy)
|
||||||
|
* [Images](#images)
|
||||||
|
|
||||||
## Technical Specifications
|
## Technical Specifications
|
||||||
|
|
||||||
@@ -41,6 +44,15 @@ MiauInv splits responsibility cleanly across modularized architecture packages:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Documentation
|
||||||
|
|
||||||
|
For deep dives into specific subsystems, database layouts, and security mechanisms, please refer to the dedicated documentation files:
|
||||||
|
|
||||||
|
* **[Database Schema & Integrity](docs/DATABASE.md):** Comprehensive breakdown of the SQLite table structures, fields, and foreign key relations.
|
||||||
|
* **[Authentication Architecture](docs/AUTHENTICATION.md):** Detailed explanation of the dual-token rotation flow, JWT lifecycle, and frontend loop protection.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The system uses a combination of a structural JSON configuration file and environment variables for system runtime flags.
|
The system uses a combination of a structural JSON configuration file and environment variables for system runtime flags.
|
||||||
@@ -129,8 +141,7 @@ Before deployment, you must generate SSL/TLS certificates since MiauInv enforces
|
|||||||
mkdir -p appdata
|
mkdir -p appdata
|
||||||
|
|
||||||
# Generate self-signed certificate and private key
|
# Generate self-signed certificate and private key
|
||||||
openssl req -x509 -newkey rsa:4096 -keyout appdata/key.pem -out certs/cert.pem -sha256 -days 365 -nodes
|
openssl req -x509 -newkey rsa:4096 -keyout appdata/key.pem -out appdata/cert.pem -sha256 -days 365 -nodes
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 1: Native Local Deployment
|
### Option 1: Native Local Deployment
|
||||||
@@ -142,7 +153,6 @@ openssl req -x509 -newkey rsa:4096 -keyout appdata/key.pem -out certs/cert.pem -
|
|||||||
export JWT_SECRET="your_minimum_thirty_two_char_secret_key_here"
|
export JWT_SECRET="your_minimum_thirty_two_char_secret_key_here"
|
||||||
go build -o miauinv main.go
|
go build -o miauinv main.go
|
||||||
./miauinv
|
./miauinv
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -167,7 +177,6 @@ services:
|
|||||||
- JWT_SECRET=SECURE_RANDOM_STRING # Must be at least 32 characters long
|
- JWT_SECRET=SECURE_RANDOM_STRING # Must be at least 32 characters long
|
||||||
volumes:
|
volumes:
|
||||||
- ./appdata:/appdata # To edit your configuration files
|
- ./appdata:/appdata # To edit your configuration files
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Execution Commands
|
#### 2. Execution Commands
|
||||||
@@ -183,7 +192,74 @@ docker-compose ps
|
|||||||
|
|
||||||
# Monitor execution system logs
|
# Monitor execution system logs
|
||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Once running successfully via Docker orchestration loops, navigate your web browser context safely to `https://localhost:8080` to interact with your MiauInv control panel workspace.
|
Once running successfully via Docker orchestration loops, navigate your web browser context safely to `https://localhost:8080` to interact with your MiauInv control panel workspace.
|
||||||
|
|
||||||
|
## Reverse Proxy Integration with Caddy
|
||||||
|
|
||||||
|
If you deploy MiauInv behind a global Caddy server, Caddy must act as an HTTPS reverse proxy. Since the MiauInv binary enforces native TLS transport, Caddy needs to be configured to establish a secure backend connection and bypass verification for self-signed backend certificates.
|
||||||
|
|
||||||
|
### 1. Docker Compose Network Configuration
|
||||||
|
Ensure your MiauInv container shares an external network with your Caddy container (e.g., a network named `proxy`). The container does not need to expose public ports since Caddy communicates with it internally over port `8080`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
miauinv:
|
||||||
|
image: git.miaurizius.de/miaurizius/miauinv:latest
|
||||||
|
container_name: MiauInv
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
environment:
|
||||||
|
- JWT_SECRET=SECURE_RANDOM_STRING
|
||||||
|
volumes:
|
||||||
|
- ./appdata:/appdata
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Caddyfile Configuration
|
||||||
|
|
||||||
|
Add the following block to your server's `Caddyfile`. The `https://` prefix forces Caddy to use TLS for the backend connection, and `tls_insecure_skip_verify` allows the proxy to accept the internal self-signed certificate generated during the prerequisites step.
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
inv.yourdomain.com {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
reverse_proxy https://miauinv:8080 {
|
||||||
|
transport http {
|
||||||
|
tls_insecure_skip_verify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
X-Content-Type-Options nosniff
|
||||||
|
Referrer-Policy strict-origin-when-cross-origin
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Apply Configuration
|
||||||
|
|
||||||
|
Reload your Caddy instance to apply the reverse proxy routing rules:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec -w /etc/caddy caddy caddy reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Images
|
||||||
|
#### Dashboard
|
||||||
|
<img src="docs/img/dashboard.png">
|
||||||
|
|
||||||
|
#### Inventory
|
||||||
|
<img src="docs/img/inventory.png">
|
||||||
|
|
||||||
|
#### Locations
|
||||||
|
<img src="docs/img/locations.png">
|
||||||
|
|
||||||
|
#### Projects
|
||||||
|
<img src="docs/img/projects.png">
|
||||||
@@ -15,8 +15,6 @@ func AuthMiddleware(secret []byte) func(http.Handler) http.Handler {
|
|||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// WICHTIG: Wenn der User auf einer öffentlichen Seite ist,
|
|
||||||
// darf die Middleware KEINEN Auth-Zwang ausüben und nicht redirecten!
|
|
||||||
if r.URL.Path == "/login" || r.URL.Path == "/register" || r.URL.Path == "/" {
|
if r.URL.Path == "/login" || r.URL.Path == "/register" || r.URL.Path == "/" {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
@@ -50,14 +48,12 @@ func AuthMiddleware(secret []byte) func(http.Handler) http.Handler {
|
|||||||
if strings.HasPrefix(r.URL.Path, "/api/") {
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||||
} else {
|
} else {
|
||||||
// Falls das Cookie korrupt oder abgelaufen ist, löschen wir es direkt,
|
|
||||||
// damit das Frontend sauber merkt, dass es weg ist.
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "access_token",
|
Name: "access_token",
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
HttpOnly: false, // Erlaubt JS das Auslesen
|
HttpOnly: false,
|
||||||
})
|
})
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
sudo docker buildx build \
|
sudo docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
-t git.miaurizius.de/miaurizius/miauinv:latest \
|
-t git.miaurizius.de/miaurizius/miauinv:latest \
|
||||||
-t git.miaurizius.de/miaurizius/miauinv:v1.0.0 \
|
-t git.miaurizius.de/miaurizius/miauinv:v1.0.2 \
|
||||||
--push .
|
--push .
|
||||||
51
docs/AUTHENTICATION.md
Normal file
51
docs/AUTHENTICATION.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Authentication Architecture
|
||||||
|
|
||||||
|
MiauInv implements a stateless JSON Web Token (JWT) architecture combined with a persistent database-backed Refresh Token mechanism to provide high security alongside seamless session retention.
|
||||||
|
|
||||||
|
## Token Lifetime and Properties
|
||||||
|
|
||||||
|
| Token Type | Transport Vector | Storage Location | Lifetime | Purpose |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| **Access Token** | HTTP-Only Cookie & Auth Header | Memory / Browser Cookies | 15 Minutes | Signed payload validating current session identity for immediate API interaction. |
|
||||||
|
| **Refresh Token** | Secure Cookie & JSON Payload | LocalStorage / Secure Cookies | 7 Days | Long-lived high-entropy string used to request a new token pair when the Access Token expires. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Token Rotation and Flow
|
||||||
|
|
||||||
|
The application coordinates token validation through cooperative interactions between Go authentication middlewares and the frontend runtime environment.
|
||||||
|
|
||||||
|
### 1. Normal Authenticated Requests
|
||||||
|
During standard interaction loops, the Go server intercepts requests via auth middleware. It checks the incoming context for validity in the following order:
|
||||||
|
1. `Authorization: Bearer <token>` request header.
|
||||||
|
2. `access_token` cookie values.
|
||||||
|
|
||||||
|
If a valid, unexpired Access Token is recovered, the middleware parses the claims (ID, username, role) and injects them into the request context before execution routes fire.
|
||||||
|
|
||||||
|
### 2. Token Refresh Flow
|
||||||
|
When an Access Token expires mid-session, the following workflow occurs automatically:
|
||||||
|
1. The backend rejects an API call or routing intent with an HTTP state indicating token expiration.
|
||||||
|
2. The frontend execution scope identifies the expiration status and reads the `refresh_token` from storage assets.
|
||||||
|
3. The client submits a POST request containing the token payload to `/api/refresh`.
|
||||||
|
4. The backend verifies the signature, looks up the hash inside the `refresh_tokens` table, and verifies that `revoked == 0` and `expires_at > now`.
|
||||||
|
5. If the validation succeeds, a brand-new Access Token and a rotated Refresh Token pair are generated, saved to secure cookies/storage, and the user session continues without explicit re-authentication.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Mitigations
|
||||||
|
|
||||||
|
### Loop Protection
|
||||||
|
To prevent broken, expired, or malformed credentials from triggering infinite network refresh loops (which degrade browser performance and strain backend lookup performance), the frontend utilizes an explicit safety lock.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Token Expired -> Check 'is_refreshing' flag -> True -> Clear Auth & Force Login
|
||||||
|
-> False -> Set flag 'true' -> Send Request
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Before issuing an evaluation request to `/api/refresh`, the application checks a temporary session variable (`is_refreshing` within `sessionStorage`). If the flag is already set to `true`, the loop protection triggers a hard clearance routine via `clearAllAuth()`, drops all token storage records, and routes the user back to the primary login view safely.
|
||||||
|
|
||||||
|
### Database Revocation
|
||||||
|
Refresh sessions can be killed immediately from the server side. When a user requests `/api/logout`, the backend switches the corresponding row state within the `refresh_tokens` database container to `revoked = 1`. Any subsequent rotation requests relying on that token family are automatically dropped, protecting against stolen credential replay attacks.
|
||||||
97
docs/DATABASE.md
Normal file
97
docs/DATABASE.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Database Documentation
|
||||||
|
|
||||||
|
MiauInv utilizes an embedded SQLite database instance for persistent data storage. Foreign key constraints are strictly enforced at the database level.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
To ensure data integrity, every database connection initialization explicitly executes the following command before handling queries:
|
||||||
|
```sql
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schema Architecture
|
||||||
|
|
||||||
|
### Entity-Relationship Summary
|
||||||
|
|
||||||
|
The database consists of primary entity tables (`users`, `items`, `locations`, `projects`) and relational junction tables (`stock`, `project_items`, `refresh_tokens`) designed to track stock distribution and access sessions.
|
||||||
|
|
||||||
|
```
|
||||||
|
[users] <--- (1:N) ---> [refresh_tokens]
|
||||||
|
[items] <--- (1:N) ---> [stock] <--- (N:1) ---> [locations]
|
||||||
|
[items] <--- (1:N) ---> [project_items] <--- (N:1) ---> [projects]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table Definitions
|
||||||
|
|
||||||
|
### 1. users
|
||||||
|
|
||||||
|
Stores user credentials and operational roles within the system.
|
||||||
|
|
||||||
|
* **id (TEXT, PK):** Unique UUID
|
||||||
|
* **username (TEXT, Unique):** Unique account identifier.
|
||||||
|
* **password (TEXT):** Hashed user password.
|
||||||
|
* **role (TEXT):** Access control flag (e.g., admin, user).
|
||||||
|
|
||||||
|
### 2. refresh_tokens
|
||||||
|
|
||||||
|
Tracks valid extended sessions linked to specific user accounts.
|
||||||
|
|
||||||
|
* **id (TEXT, PK):** Unique identifier.
|
||||||
|
* **user_id (TEXT, FK):** References `users(id)`.
|
||||||
|
* **token_hash (TEXT):** Cryptographic hash of the active refresh token.
|
||||||
|
* **expires_at (INTEGER):** Unix timestamp indicating token expiration.
|
||||||
|
* **created_at (INTEGER):** Unix timestamp indicating session creation.
|
||||||
|
* **revoked (INTEGER):** Boolean flag (0 or 1) indicating if the session was manually invalidated.
|
||||||
|
* **device_info (TEXT, Optional):** Client metadata for auditing.
|
||||||
|
|
||||||
|
### 3. items
|
||||||
|
|
||||||
|
Represents individual tracked assets.
|
||||||
|
|
||||||
|
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||||
|
* **name (TEXT):** Asset designation.
|
||||||
|
* **category (TEXT, Optional):** Grouping classification.
|
||||||
|
* **description (TEXT, Optional):** Detailed asset context.
|
||||||
|
* **total_quantity (INTEGER):** Absolute global stock baseline counter.
|
||||||
|
|
||||||
|
### 4. locations
|
||||||
|
|
||||||
|
Defines logical or physical facilities.
|
||||||
|
|
||||||
|
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||||
|
* **name (TEXT, Unique):** Unique facility naming constraint.
|
||||||
|
|
||||||
|
### 5. projects
|
||||||
|
|
||||||
|
Defines distinct tasks or allocation targets.
|
||||||
|
|
||||||
|
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||||
|
* **name (TEXT, Unique):** Unique operational tracking name.
|
||||||
|
* **description (TEXT, Optional):** Scope description.
|
||||||
|
|
||||||
|
### 6. stock
|
||||||
|
|
||||||
|
Junction table mapping physical asset distributions across facilities.
|
||||||
|
|
||||||
|
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||||
|
* **item_id (INTEGER, FK):** References `items(id)`.
|
||||||
|
* **location_id (INTEGER, FK):** References `locations(id)`.
|
||||||
|
* **quantity (INTEGER):** Specific quantity present at this location node.
|
||||||
|
|
||||||
|
### 7. project_items
|
||||||
|
|
||||||
|
Junction table tracking asset assignments dedicated to specific ongoing project environments.
|
||||||
|
|
||||||
|
* **id (INTEGER, PK, Autoincrement):** Primary key.
|
||||||
|
* **item_id (INTEGER, FK):** References `items(id)`.
|
||||||
|
* **project_id (INTEGER, FK):** References `projects(id)`.
|
||||||
|
* **quantity (INTEGER):** Quantity allocated to this project context.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Integrity Constraints
|
||||||
|
|
||||||
|
* **Foreign Keys:** Because standard `ON DELETE` cascades are not defined explicitly in the schema rules, SQLite blocks parent deletion actions if dependent rows exist in `stock` or `project_items`. You must clear out stock allocations and project associations manually before deleting an item, location, or project.
|
||||||
|
* **Uniqueness:** String uniqueness constraints protect against duplicate namespace registration on `users(username)`, `locations(name)`, and `projects(name)`.
|
||||||
BIN
docs/img/dashboard.png
Normal file
BIN
docs/img/dashboard.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/img/inventory.png
Normal file
BIN
docs/img/inventory.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/img/locations.png
Normal file
BIN
docs/img/locations.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/img/projects.png
Normal file
BIN
docs/img/projects.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -1,6 +1,7 @@
|
|||||||
package frontend
|
package frontend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"MiauInv/storage"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -53,7 +54,28 @@ func Home(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func Dashboard(w http.ResponseWriter, r *http.Request) {
|
func Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("Content-Type", "text/html")
|
||||||
err := dashboard.ExecuteTemplate(w, "base.html", struct {
|
|
||||||
|
var itemHive, projectHive, locationHive int
|
||||||
|
|
||||||
|
err := storage.DB.QueryRow("SELECT COUNT(*) FROM items").Scan(&itemHive)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to count items", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storage.DB.QueryRow("SELECT COUNT(*) FROM projects").Scan(&projectHive)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to count projects", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storage.DB.QueryRow("SELECT COUNT(*) FROM locations").Scan(&locationHive)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to count locations", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dashboard.ExecuteTemplate(w, "base.html", struct {
|
||||||
Title string
|
Title string
|
||||||
Stats struct {
|
Stats struct {
|
||||||
Items int
|
Items int
|
||||||
@@ -67,9 +89,9 @@ func Dashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
Projects int
|
Projects int
|
||||||
Locations int
|
Locations int
|
||||||
}{
|
}{
|
||||||
Items: 1,
|
Items: itemHive,
|
||||||
Projects: 1,
|
Projects: projectHive,
|
||||||
Locations: 3,
|
Locations: locationHive,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ func APIRegister(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(user.Password) > 72 {
|
||||||
|
log.Println("POST [api/register] User password too long")
|
||||||
|
http.Error(w, "Password exceeds the maximum allowed length of 72 characters", http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
hashed, err := auth.HashPassword(user.Password)
|
hashed, err := auth.HashPassword(user.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("POST [api/register] " + r.RemoteAddr + ": " + err.Error())
|
log.Println("POST [api/register] " + r.RemoteAddr + ": " + err.Error())
|
||||||
|
|||||||
@@ -573,7 +573,6 @@ func Associations(w http.ResponseWriter, r *http.Request) {
|
|||||||
idStr := r.URL.Query().Get("id")
|
idStr := r.URL.Query().Get("id")
|
||||||
projectIDStr := r.URL.Query().Get("project_id")
|
projectIDStr := r.URL.Query().Get("project_id")
|
||||||
|
|
||||||
// Optionaler Filter: Alle Items für ein bestimmtes Projekt holen (?project_id=X)
|
|
||||||
if projectIDStr != "" {
|
if projectIDStr != "" {
|
||||||
pID, _ := strconv.Atoi(projectIDStr)
|
pID, _ := strconv.Atoi(projectIDStr)
|
||||||
rows, err := storage.DB.Query("SELECT id, item_id, project_id, quantity FROM project_items WHERE project_id = ?", pID)
|
rows, err := storage.DB.Query("SELECT id, item_id, project_id, quantity FROM project_items WHERE project_id = ?", pID)
|
||||||
@@ -593,7 +592,6 @@ func Associations(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Einzelne Assoziation anhand der Tabellen-ID (?id=X)
|
|
||||||
if idStr != "" {
|
if idStr != "" {
|
||||||
id, _ := strconv.Atoi(idStr)
|
id, _ := strconv.Atoi(idStr)
|
||||||
var pi models.ProjectItem
|
var pi models.ProjectItem
|
||||||
@@ -607,7 +605,6 @@ func Associations(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gar kein Parameter -> Komplett-Dump aller Zuweisungen
|
|
||||||
rows, err := storage.DB.Query("SELECT id, item_id, project_id, quantity FROM project_items")
|
rows, err := storage.DB.Query("SELECT id, item_id, project_id, quantity FROM project_items")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||||
|
|||||||
Reference in New Issue
Block a user