Compare commits
7 Commits
851edb06a4
...
fa1d6d4927
| Author | SHA1 | Date | |
|---|---|---|---|
|
fa1d6d4927
|
|||
|
b1bbe12734
|
|||
|
b07297f323
|
|||
|
1303b35387
|
|||
|
9cefd615ce
|
|||
|
1ab16c9efa
|
|||
|
1cfb44b9a4
|
@@ -58,6 +58,7 @@ The goal is simple: everything the repair shop needs to track is located in one
|
|||||||
- Charts — monthly revenue on reports (Chart.js)
|
- Charts — monthly revenue on reports (Chart.js)
|
||||||
- Structured logging — `log/slog` (JSON in production, text in development); separate auth log in fail2ban format
|
- Structured logging — `log/slog` (JSON in production, text in development); separate auth log in fail2ban format
|
||||||
- Automated tests — unit and integration over a SQLite database (crypto, RBAC, login flows, form validators, reports)
|
- Automated tests — unit and integration over a SQLite database (crypto, RBAC, login flows, form validators, reports)
|
||||||
|
- **Demo mode** (`NTECH_ENV=demo`) — auto-created demo user, pre-filled login form, restricted backup count, blocked password/2FA changes
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
|
|
||||||
@@ -97,11 +98,143 @@ The goal is simple: everything the repair shop needs to track is located in one
|
|||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd GoNtech
|
cd GoNtech
|
||||||
|
|
||||||
# 2. Copy the configuration file
|
# 2. Run in development mode (reads files from disk, no HTTPS required)
|
||||||
cp ntech.env.example ntech.env
|
|
||||||
# Open ntech.env and set the values (see the table below)
|
|
||||||
|
|
||||||
# 3. Load environment variables and run in the development environment
|
|
||||||
export $(grep -v '^#' ntech.env | xargs)
|
|
||||||
go run ./cmd/ntech
|
go run ./cmd/ntech
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The application opens at `http://localhost:8080`. On first run the setup wizard starts automatically.
|
||||||
|
|
||||||
|
### Production Build
|
||||||
|
|
||||||
|
Use the interactive build script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
It asks for the version, environment (production/development), platform (Linux/Windows/both), optional UPX compression, and whether to push a Docker image to Gitea and GitHub Container Registry.
|
||||||
|
|
||||||
|
Or build manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
|
-ldflags "-X main.Verzija=1.0.0 -s -w" \
|
||||||
|
-trimpath \
|
||||||
|
-o ntech ./cmd/ntech
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is a single static binary with no external dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The application reads environment variables on startup. In development, place them in `ntech.env` alongside the SQLite database file. In production/demo the program creates `ntech.env` automatically in the same directory as the database.
|
||||||
|
|
||||||
|
`ntech.env` is **never committed** to Git.
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| ---------------- | ------------- | ----------------------------------------------------------------- |
|
||||||
|
| `NTECH_ENV` | `development` | Mode: `development`, `production`, or `demo` |
|
||||||
|
| `NTECH_PORT` | `8080` | HTTP port |
|
||||||
|
| `NTECH_DB` | `sqlite` | Database type: `sqlite` or `postgres` |
|
||||||
|
| `NTECH_SQLITE` | `ntech.db` | Path to the SQLite file |
|
||||||
|
| `NTECH_DSN` | — | PostgreSQL connection string |
|
||||||
|
| `NTECH_SECRET` | — | Session signing key (min. 32 bytes); auto-generated if missing |
|
||||||
|
| `NTECH_TOTP_KEY` | — | AES-256 key for TOTP secret encryption; auto-generated if missing |
|
||||||
|
|
||||||
|
`NTECH_SECRET` and `NTECH_TOTP_KEY` are generated automatically on the first run and saved to `ntech.env`. **Back this file up** — losing `NTECH_TOTP_KEY` invalidates all 2FA secrets stored in the database.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
Docker images are published to:
|
||||||
|
- `ghcr.io/dalibor31/ntech:latest`
|
||||||
|
- `git.vm-net.in.rs/dasko/ntech:latest`
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
ntech:
|
||||||
|
image: ghcr.io/dalibor31/ntech:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NTECH_ENV: production
|
||||||
|
NTECH_PORT: "8000"
|
||||||
|
NTECH_SQLITE: /app/data/ntech.db
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data # database + ntech.env (secrets)
|
||||||
|
- ./uploads:/app/uploads # uploaded images
|
||||||
|
- ./logs:/app/logs # structured + auth logs
|
||||||
|
- ./backups:/app/backups # automatic database backups
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
On the **first start** the setup wizard runs and creates the first admin user. After that, `./data/ntech.env` contains the auto-generated secrets — **back it up**.
|
||||||
|
|
||||||
|
Place the app behind a reverse proxy (Caddy, nginx) that terminates HTTPS. Secure cookies require HTTPS.
|
||||||
|
|
||||||
|
Example Caddy config:
|
||||||
|
|
||||||
|
```
|
||||||
|
your.domain.com {
|
||||||
|
reverse_proxy ntech:8000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo Mode
|
||||||
|
|
||||||
|
Demo mode runs a fully functional copy with a pre-created `Demo` / `Demo1234` admin account. Password and 2FA changes are blocked. Backup is limited to 2 copies.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml (demo)
|
||||||
|
services:
|
||||||
|
ntech-demo:
|
||||||
|
image: ghcr.io/dalibor31/ntech:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NTECH_ENV: demo
|
||||||
|
NTECH_PORT: "8000"
|
||||||
|
NTECH_SQLITE: /app/data/ntech.db
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./backups:/app/backups
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
Demo also requires HTTPS (Caddy or similar) because Secure cookies are enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ntech/
|
||||||
|
├── cmd/
|
||||||
|
│ └── ntech/ # entry point
|
||||||
|
├── internal/
|
||||||
|
│ ├── auth/ # login, sessions, fail2ban log
|
||||||
|
│ ├── config/ # settings, setup wizard
|
||||||
|
│ ├── db/ # database layer
|
||||||
|
│ │ └── sqlite/ # SQLite implementation
|
||||||
|
│ ├── handler/ # HTTP handlers
|
||||||
|
│ ├── middleware/ # CSRF, security headers, authentication
|
||||||
|
│ └── model/ # shared data types
|
||||||
|
├── web/
|
||||||
|
│ ├── static/ # CSS, JavaScript, images, logos
|
||||||
|
│ └── templates/ # HTML templates
|
||||||
|
├── migrations/ # SQL migrations (001_desc.sql, 002_desc.sql, ...)
|
||||||
|
├── logs/ # auth.log and other logs
|
||||||
|
├── backups/ # database backups
|
||||||
|
├── start.sh # interactive build and Docker push script
|
||||||
|
├── Dockerfile
|
||||||
|
├── go.mod
|
||||||
|
└── go.sum
|
||||||
|
```
|
||||||
|
|||||||
+97
-25
@@ -58,6 +58,7 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
|
|||||||
- Grafikoni — mesečni prihod na izveštajima (Chart.js)
|
- Grafikoni — mesečni prihod na izveštajima (Chart.js)
|
||||||
- Strukturisano logovanje — `log/slog` (JSON u produkciji, tekst u razvoju); zaseban auth log u fail2ban formatu
|
- Strukturisano logovanje — `log/slog` (JSON u produkciji, tekst u razvoju); zaseban auth log u fail2ban formatu
|
||||||
- Automatski testovi — jedinični i integracioni nad SQLite bazom (kripto, RBAC, tokovi prijave, validatori forme, izveštaji)
|
- Automatski testovi — jedinični i integracioni nad SQLite bazom (kripto, RBAC, tokovi prijave, validatori forme, izveštaji)
|
||||||
|
- **Demo mod** (`NTECH_ENV=demo`) — automatski kreiran demo korisnik, pre-popunjeni login, ograničen bekap, blokirana promena lozinke i 2FA
|
||||||
|
|
||||||
### Planirano
|
### Planirano
|
||||||
|
|
||||||
@@ -97,30 +98,29 @@ Cilj je jednostavan: sve što servis treba da prati nalazi se na jednom mestu, b
|
|||||||
git clone <url-repozitorijuma>
|
git clone <url-repozitorijuma>
|
||||||
cd GoNtech
|
cd GoNtech
|
||||||
|
|
||||||
# 2. Kopiranje konfiguracionog fajla
|
# 2. Pokretanje u razvojnom modu (čita fajlove sa diska, ne zahteva HTTPS)
|
||||||
cp ntech.env.example ntech.env
|
|
||||||
# Otvori ntech.env i postavi vrednosti (videti tabelu ispod)
|
|
||||||
|
|
||||||
# 3. Učitavanje promenljivih i pokretanje u razvojnom okruženju
|
|
||||||
export $(grep -v '^#' ntech.env | xargs)
|
|
||||||
go run ./cmd/ntech
|
go run ./cmd/ntech
|
||||||
```
|
```
|
||||||
|
|
||||||
Program se otvara na `http://localhost:8080` (ili na portu definisanom u `ntech.env`).
|
Program se otvara na `http://localhost:8080`. Pri prvom pokretanju automatski se pokreće setup wizard.
|
||||||
|
|
||||||
Pri prvom pokretanju automatski se pokreće setup wizard.
|
|
||||||
|
|
||||||
### Produkcioni build
|
### Produkcioni build
|
||||||
|
|
||||||
```bash
|
Koristi interaktivnu skriptu:
|
||||||
# Pomoću build.sh skripte (prima opcioni argument verzije)
|
|
||||||
./build.sh 1.0.0
|
|
||||||
|
|
||||||
# Ili ručno
|
```bash
|
||||||
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build \
|
./start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Skripta pita za verziju, okruženje (production/development), platformu (Linux/Windows/obe), opcionalnu UPX kompresiju i da li da gurne Docker image na Gitea i GitHub Container Registry.
|
||||||
|
|
||||||
|
Ili ručno:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
|
||||||
-ldflags "-X main.Verzija=1.0.0 -s -w" \
|
-ldflags "-X main.Verzija=1.0.0 -s -w" \
|
||||||
|
-trimpath \
|
||||||
-o ntech ./cmd/ntech
|
-o ntech ./cmd/ntech
|
||||||
./ntech
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Rezultat je jedan statički binarni fajl bez zavisnosti.
|
Rezultat je jedan statički binarni fajl bez zavisnosti.
|
||||||
@@ -129,15 +129,87 @@ Rezultat je jedan statički binarni fajl bez zavisnosti.
|
|||||||
|
|
||||||
## Promenljive okruženja
|
## Promenljive okruženja
|
||||||
|
|
||||||
Kopirati `ntech.env.example` u `ntech.env` i popuniti vrednosti. Fajl `ntech.env` se **ne commituje** u Git.
|
Program čita promenljive okruženja pri pokretanju. U razvojnom modu staviti ih u `ntech.env` pored SQLite baze. U production/demo modu program sam kreira `ntech.env` u istom folderu gde je baza.
|
||||||
|
|
||||||
| Promenljiva | Podrazumevano | Opis |
|
Fajl `ntech.env` se **ne commituje** u Git.
|
||||||
| -------------- | ------------- | -------------------------------------------- |
|
|
||||||
| `NTECH_ENV` | `development` | Okruženje: `development` ili `production` |
|
| Promenljiva | Podrazumevano | Opis |
|
||||||
| `NTECH_PORT` | `8080` | HTTP port |
|
| ---------------- | ------------- | ------------------------------------------------------------------ |
|
||||||
| `NTECH_DB` | `sqlite` | Tip baze: `sqlite` ili `postgres` |
|
| `NTECH_ENV` | `development` | Mod: `development`, `production` ili `demo` |
|
||||||
| `NTECH_SQLITE` | `ntech.db` | Putanja do SQLite fajla |
|
| `NTECH_PORT` | `8080` | HTTP port |
|
||||||
| `NTECH_DSN` | — | PostgreSQL connection string |
|
| `NTECH_DB` | `sqlite` | Tip baze: `sqlite` ili `postgres` |
|
||||||
|
| `NTECH_SQLITE` | `ntech.db` | Putanja do SQLite fajla |
|
||||||
|
| `NTECH_DSN` | — | PostgreSQL connection string |
|
||||||
|
| `NTECH_SECRET` | — | Ključ za potpisivanje sesija (min. 32 bajta); auto-generiše se |
|
||||||
|
| `NTECH_TOTP_KEY` | — | AES-256 ključ za šifrovanje TOTP tajni; auto-generiše se |
|
||||||
|
|
||||||
|
`NTECH_SECRET` i `NTECH_TOTP_KEY` se automatski generišu pri prvom pokretanju i upisuju u `ntech.env`. **Sačuvaj backup ovog fajla** — gubitak `NTECH_TOTP_KEY` onemogućuje prijavu svim korisnicima koji imaju 2FA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker deployment
|
||||||
|
|
||||||
|
Docker image je dostupan na:
|
||||||
|
- `ghcr.io/dalibor31/ntech:latest`
|
||||||
|
- `git.vm-net.in.rs/dasko/ntech:latest`
|
||||||
|
|
||||||
|
### Produkcija
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
services:
|
||||||
|
ntech:
|
||||||
|
image: ghcr.io/dalibor31/ntech:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NTECH_ENV: production
|
||||||
|
NTECH_PORT: "8000"
|
||||||
|
NTECH_SQLITE: /app/data/ntech.db
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data # baza + ntech.env (tajne)
|
||||||
|
- ./uploads:/app/uploads # uploadovane slike
|
||||||
|
- ./logs:/app/logs # strukturisani + auth log
|
||||||
|
- ./backups:/app/backups # automatski bekap baze
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
Pri **prvom pokretanju** pokreće se setup wizard za kreiranje prvog admin korisnika. Nakon toga, `./data/ntech.env` sadrži auto-generisane tajne — **sačuvaj backup**.
|
||||||
|
|
||||||
|
Stavi program iza reverznog proksija (Caddy, nginx) koji terminira HTTPS. Secure kolačići zahtevaju HTTPS.
|
||||||
|
|
||||||
|
Primer Caddy konfiguracije:
|
||||||
|
|
||||||
|
```
|
||||||
|
tvoj.domen.com {
|
||||||
|
reverse_proxy ntech:8000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo mod
|
||||||
|
|
||||||
|
Demo mod pokreće potpuno funkcionalnu kopiju sa pre-kreiranim nalogom `Demo` / `Demo1234` (admin). Promena lozinke i 2FA su blokirani. Bekap je ograničen na 2 kopije.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml (demo)
|
||||||
|
services:
|
||||||
|
ntech-demo:
|
||||||
|
image: ghcr.io/dalibor31/ntech:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NTECH_ENV: demo
|
||||||
|
NTECH_PORT: "8000"
|
||||||
|
NTECH_SQLITE: /app/data/ntech.db
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./backups:/app/backups
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
Demo takođe zahteva HTTPS (Caddy ili slično) jer su Secure kolačići uključeni.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -161,8 +233,8 @@ ntech/
|
|||||||
├── migrations/ # SQL migracije (001_opis.sql, 002_opis.sql, ...)
|
├── migrations/ # SQL migracije (001_opis.sql, 002_opis.sql, ...)
|
||||||
├── logs/ # auth.log i ostali logovi
|
├── logs/ # auth.log i ostali logovi
|
||||||
├── backups/ # rezervne kopije baze
|
├── backups/ # rezervne kopije baze
|
||||||
├── build.sh # skripta za produkcioni build
|
├── start.sh # interaktivna skripta za build i Docker push
|
||||||
├── ntech.env # lokalna konfiguracija (ne commituje se)
|
├── Dockerfile
|
||||||
├── go.mod
|
├── go.mod
|
||||||
└── go.sum
|
└── go.sum
|
||||||
```
|
```
|
||||||
|
|||||||
+78
-19
@@ -19,6 +19,7 @@ import (
|
|||||||
"ntech"
|
"ntech"
|
||||||
"ntech/internal/auth"
|
"ntech/internal/auth"
|
||||||
"ntech/internal/config"
|
"ntech/internal/config"
|
||||||
|
"ntech/internal/db"
|
||||||
"ntech/internal/db/sqlite"
|
"ntech/internal/db/sqlite"
|
||||||
"ntech/internal/handler"
|
"ntech/internal/handler"
|
||||||
ntechmw "ntech/internal/middleware"
|
ntechmw "ntech/internal/middleware"
|
||||||
@@ -47,7 +48,19 @@ func podesiLog() {
|
|||||||
func main() {
|
func main() {
|
||||||
mime.AddExtensionType(".js", "text/javascript")
|
mime.AddExtensionType(".js", "text/javascript")
|
||||||
mime.AddExtensionType(".css", "text/css")
|
mime.AddExtensionType(".css", "text/css")
|
||||||
godotenv.Load("ntech.env")
|
putanjaBaze := os.Getenv("NTECH_SQLITE")
|
||||||
|
if putanjaBaze == "" {
|
||||||
|
putanjaBaze = "ntech.db"
|
||||||
|
}
|
||||||
|
envFajl := config.PutanjaNtechEnv(putanjaBaze)
|
||||||
|
// u production/demo modu sve podešavanja dolaze iz env promenljivih —
|
||||||
|
// kreiraj prazan fajl ako ne postoji da se ne pokrene setup wizard
|
||||||
|
if env := os.Getenv("NTECH_ENV"); env == "production" || env == "demo" {
|
||||||
|
if _, err := os.Stat(envFajl); os.IsNotExist(err) {
|
||||||
|
os.WriteFile(envFajl, []byte(""), 0600)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
godotenv.Load(envFajl)
|
||||||
podesiLog()
|
podesiLog()
|
||||||
auth.InitAuthLog()
|
auth.InitAuthLog()
|
||||||
|
|
||||||
@@ -62,16 +75,16 @@ func main() {
|
|||||||
if _, err := os.Stat("migrations"); err == nil {
|
if _, err := os.Stat("migrations"); err == nil {
|
||||||
migrFS = os.DirFS(".")
|
migrFS = os.DirFS(".")
|
||||||
}
|
}
|
||||||
// staticFS je rootovan na "web/static" — u produkciji embed, u razvoju disk
|
// staticFS je rootovan na "web/static" — u produkciji i demo modu embed, u razvoju disk
|
||||||
var staticFS fs.FS
|
var staticFS fs.FS
|
||||||
if os.Getenv("NTECH_ENV") == "production" {
|
if ntechEnv := os.Getenv("NTECH_ENV"); ntechEnv == "production" || ntechEnv == "demo" {
|
||||||
staticFS, _ = fs.Sub(assets.StaticFS, "web/static")
|
staticFS, _ = fs.Sub(assets.StaticFS, "web/static")
|
||||||
} else {
|
} else {
|
||||||
staticFS = os.DirFS("web/static")
|
staticFS = os.DirFS("web/static")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.JelPrvoPokretanje() {
|
if config.JelPrvoPokretanje(envFajl) {
|
||||||
config.PokreniSetup(templFS)
|
config.PokreniSetup(templFS, envFajl)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +93,6 @@ func main() {
|
|||||||
port = "8080"
|
port = "8080"
|
||||||
}
|
}
|
||||||
|
|
||||||
putanjaBaze := os.Getenv("NTECH_SQLITE")
|
|
||||||
if putanjaBaze == "" {
|
|
||||||
putanjaBaze = "ntech.db"
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := sqlite.OtvoriDB(putanjaBaze)
|
db, err := sqlite.OtvoriDB(putanjaBaze)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Greška pri otvaranju baze", "error", err)
|
slog.Error("Greška pri otvaranju baze", "error", err)
|
||||||
@@ -111,7 +119,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
// ključ za šifrovanje TOTP tajni u mirovanju (AES-256-GCM)
|
||||||
totpKljuc, err := ucitajTotpKljuc()
|
totpKljuc, err := ucitajTotpKljuc(envFajl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Greška pri učitavanju ključa za TOTP", "error", err)
|
slog.Error("Greška pri učitavanju ključa za TOTP", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -124,12 +132,25 @@ func main() {
|
|||||||
slog.Info("šifrovane postojeće TOTP tajne", "broj", br)
|
slog.Info("šifrovane postojeće TOTP tajne", "broj", br)
|
||||||
}
|
}
|
||||||
|
|
||||||
napraviBackup(db, putanjaBaze)
|
{
|
||||||
|
max := procitajIntPodesavanje(db, "backup_broj_kopija", 7)
|
||||||
|
if os.Getenv("NTECH_ENV") == "demo" {
|
||||||
|
max = 2
|
||||||
|
}
|
||||||
|
napraviBackup(db, putanjaBaze, max)
|
||||||
|
}
|
||||||
|
|
||||||
os.MkdirAll("web/static/uploads", 0755)
|
os.MkdirAll("web/static/uploads", 0755)
|
||||||
|
|
||||||
h := handler.Novi(db, totpKljuc)
|
h := handler.Novi(db, totpKljuc)
|
||||||
h.Verzija = Verzija
|
h.Verzija = Verzija
|
||||||
|
h.JelDemo = os.Getenv("NTECH_ENV") == "demo"
|
||||||
|
if h.JelDemo {
|
||||||
|
h.Verzija = "DEMO verzija"
|
||||||
|
if err := postaviDemoKorisnika(context.Background(), h.KorisniciRepo); err != nil {
|
||||||
|
slog.Warn("demo: greška pri postavljanju demo korisnika", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
// verzija statičkih fajlova za cache-busting — menja se pri svakom pokretanju,
|
// verzija statičkih fajlova za cache-busting — menja se pri svakom pokretanju,
|
||||||
// pa novi build/restart natera brauzer da povuče sveži CSS/JS (umesto starog iz keša)
|
// pa novi build/restart natera brauzer da povuče sveži CSS/JS (umesto starog iz keša)
|
||||||
h.AssetV = strconv.FormatInt(time.Now().Unix(), 36)
|
h.AssetV = strconv.FormatInt(time.Now().Unix(), 36)
|
||||||
@@ -137,7 +158,7 @@ func main() {
|
|||||||
// čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji
|
// čuva odabrani FS (disk ili embed) za hot-reload u razvoju i za keš u produkciji
|
||||||
h.TemplatesFS = templFS
|
h.TemplatesFS = templFS
|
||||||
|
|
||||||
if os.Getenv("NTECH_ENV") == "production" {
|
if ntechEnv := os.Getenv("NTECH_ENV"); ntechEnv == "production" || ntechEnv == "demo" {
|
||||||
kes, err := handler.KreirajKes(templFS)
|
kes, err := handler.KreirajKes(templFS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Greška pri kreiranju keša šablona", "error", err)
|
slog.Error("Greška pri kreiranju keša šablona", "error", err)
|
||||||
@@ -160,7 +181,11 @@ func main() {
|
|||||||
})
|
})
|
||||||
time.Sleep(time.Duration(sati) * time.Hour)
|
time.Sleep(time.Duration(sati) * time.Hour)
|
||||||
h.SaBazom(func(db *sql.DB) {
|
h.SaBazom(func(db *sql.DB) {
|
||||||
napraviBackup(db, putanjaBaze)
|
max := procitajIntPodesavanje(db, "backup_broj_kopija", 7)
|
||||||
|
if h.JelDemo {
|
||||||
|
max = 2
|
||||||
|
}
|
||||||
|
napraviBackup(db, putanjaBaze, max)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -194,7 +219,8 @@ func main() {
|
|||||||
// ostali statični fajlovi: disk ako postoji web/static, inače embed.
|
// ostali statični fajlovi: disk ako postoji web/static, inače embed.
|
||||||
// U produkciji dug immutable keš (URL nosi ?v=verzija za cache-busting pri novom buildu);
|
// U produkciji dug immutable keš (URL nosi ?v=verzija za cache-busting pri novom buildu);
|
||||||
// u razvoju bez keša, da izmene CSS/JS odmah budu vidljive bez ručnog osvežavanja.
|
// u razvoju bez keša, da izmene CSS/JS odmah budu vidljive bez ručnog osvežavanja.
|
||||||
produkcija := os.Getenv("NTECH_ENV") == "production"
|
ntechEnvStr := os.Getenv("NTECH_ENV")
|
||||||
|
produkcija := ntechEnvStr == "production" || ntechEnvStr == "demo"
|
||||||
r.Handle("/static/*", http.StripPrefix("/static/",
|
r.Handle("/static/*", http.StripPrefix("/static/",
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
if produkcija {
|
if produkcija {
|
||||||
@@ -382,7 +408,7 @@ func main() {
|
|||||||
//
|
//
|
||||||
// VAŽNO: ako se ovaj ključ izgubi ili promeni, postojeće šifrovane TOTP tajne se
|
// VAŽNO: ako se ovaj ključ izgubi ili promeni, postojeće šifrovane TOTP tajne se
|
||||||
// više ne mogu dešifrovati (korisnici moraju ponovo aktivirati 2FA).
|
// više ne mogu dešifrovati (korisnici moraju ponovo aktivirati 2FA).
|
||||||
func ucitajTotpKljuc() ([]byte, error) {
|
func ucitajTotpKljuc(envFajl string) ([]byte, error) {
|
||||||
if v := os.Getenv("NTECH_TOTP_KEY"); v != "" {
|
if v := os.Getenv("NTECH_TOTP_KEY"); v != "" {
|
||||||
kljuc, err := base64.StdEncoding.DecodeString(v)
|
kljuc, err := base64.StdEncoding.DecodeString(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -400,7 +426,7 @@ func ucitajTotpKljuc() ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("generisanje ključa: %w", err)
|
return nil, fmt.Errorf("generisanje ključa: %w", err)
|
||||||
}
|
}
|
||||||
enkodiran := base64.StdEncoding.EncodeToString(kljuc)
|
enkodiran := base64.StdEncoding.EncodeToString(kljuc)
|
||||||
f, err := os.OpenFile("ntech.env", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
f, err := os.OpenFile(envFajl, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("otvaranje ntech.env: %w", err)
|
return nil, fmt.Errorf("otvaranje ntech.env: %w", err)
|
||||||
}
|
}
|
||||||
@@ -413,9 +439,42 @@ func ucitajTotpKljuc() ([]byte, error) {
|
|||||||
return kljuc, nil
|
return kljuc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// postaviDemoKorisnika osigurava da pri pokretanju demo instance postoji korisnik "Demo"
|
||||||
|
// sa ulogom "admin" i resetovanom lozinkom. Poziva se samo kada je NTECH_ENV=demo.
|
||||||
|
func postaviDemoKorisnika(ctx context.Context, repo db.KorisniciRepository) error {
|
||||||
|
const (
|
||||||
|
demoIme = "Demo"
|
||||||
|
demoLozinka = "Demo1234"
|
||||||
|
)
|
||||||
|
hash, err := auth.HashujLozinku(demoLozinka)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ntech: postaviDemoKorisnika: %w", err)
|
||||||
|
}
|
||||||
|
korisnik, err := repo.DohvatiPoImenu(ctx, demoIme)
|
||||||
|
if err != nil {
|
||||||
|
// korisnik ne postoji — kreiraj ga
|
||||||
|
if _, err := repo.Kreiraj(ctx, demoIme, hash, "admin"); err != nil {
|
||||||
|
return fmt.Errorf("ntech: postaviDemoKorisnika: kreiranje: %w", err)
|
||||||
|
}
|
||||||
|
slog.Info("demo korisnik kreiran", "korisnik", demoIme)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// korisnik postoji — resetuj lozinku i osiguraj da je aktivan
|
||||||
|
if err := repo.PromeniLozinku(ctx, korisnik.ID, hash); err != nil {
|
||||||
|
return fmt.Errorf("ntech: postaviDemoKorisnika: lozinka: %w", err)
|
||||||
|
}
|
||||||
|
if !korisnik.Aktivan {
|
||||||
|
if err := repo.AzurirajAktivan(ctx, korisnik.ID, true); err != nil {
|
||||||
|
return fmt.Errorf("ntech: postaviDemoKorisnika: aktivan: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slog.Info("demo korisnik resetovan", "korisnik", demoIme)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// napraviBackup kreira konzistentnu kopiju baze i briše najstarije preko zadatog broja kopija.
|
// napraviBackup kreira konzistentnu kopiju baze i briše najstarije preko zadatog broja kopija.
|
||||||
// Koristi već otvorenu vezu ka bazi (VACUUM INTO je bezbedan na pooled konekciji).
|
// Koristi već otvorenu vezu ka bazi (VACUUM INTO je bezbedan na pooled konekciji).
|
||||||
func napraviBackup(db *sql.DB, putanjaBaze string) {
|
func napraviBackup(db *sql.DB, putanjaBaze string, maxKopija int) {
|
||||||
if _, err := os.Stat(putanjaBaze); os.IsNotExist(err) {
|
if _, err := os.Stat(putanjaBaze); os.IsNotExist(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -435,7 +494,7 @@ func napraviBackup(db *sql.DB, putanjaBaze string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("backup kreiran", "putanja", odrediste)
|
slog.Info("backup kreiran", "putanja", odrediste)
|
||||||
ocistiStareBackupe(folder, procitajIntPodesavanje(db, "backup_broj_kopija", 7))
|
ocistiStareBackupe(folder, maxKopija)
|
||||||
}
|
}
|
||||||
|
|
||||||
// procitajIntPodesavanje vraća celobrojnu vrednost podešavanja iz baze,
|
// procitajIntPodesavanje vraća celobrojnu vrednost podešavanja iz baze,
|
||||||
|
|||||||
@@ -4,8 +4,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PutanjaNtechEnv vraća putanju do ntech.env fajla na osnovu putanje SQLite baze.
|
||||||
|
// Fajl se čuva u istom direktorijumu kao i baza, tako da volume mount direktorijuma
|
||||||
|
// pokriva i bazu i konfiguraciju.
|
||||||
|
func PutanjaNtechEnv(sqlitePutanja string) string {
|
||||||
|
dir := filepath.Dir(sqlitePutanja)
|
||||||
|
return filepath.Join(dir, "ntech.env")
|
||||||
|
}
|
||||||
|
|
||||||
// lista portova koje proveravamo pri prvom pokretanju
|
// lista portova koje proveravamo pri prvom pokretanju
|
||||||
var kandidatPortovi = []int{8080, 3000, 8000, 9090}
|
var kandidatPortovi = []int{8080, 3000, 8000, 9090}
|
||||||
|
|
||||||
@@ -32,8 +41,8 @@ func NadjiSlobodanPort() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// proverava da li je ovo prvo pokretanje programa
|
// proverava da li je ovo prvo pokretanje programa
|
||||||
func JelPrvoPokretanje() bool {
|
func JelPrvoPokretanje(envFajl string) bool {
|
||||||
_, err := os.Stat("ntech.env")
|
_, err := os.Stat(envFajl)
|
||||||
return os.IsNotExist(err)
|
return os.IsNotExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +65,7 @@ func StatusPortova() []PortStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SacuvajEnv upisuje izabrani port u ntech.env fajl
|
// SacuvajEnv upisuje izabrani port u ntech.env fajl
|
||||||
func SacuvajEnv(port int) error {
|
func SacuvajEnv(port int, envFajl string) error {
|
||||||
sadrzaj := fmt.Sprintf("NTECH_PORT=%d\n", port)
|
sadrzaj := fmt.Sprintf("NTECH_PORT=%d\n", port)
|
||||||
return os.WriteFile("ntech.env", []byte(sadrzaj), 0600)
|
return os.WriteFile(envFajl, []byte(sadrzaj), 0600)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ func nadjiLokalneAdrese() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PokreniSetup pokreće HTTP server za prvo podešavanje i čeka da korisnik završi
|
// PokreniSetup pokreće HTTP server za prvo podešavanje i čeka da korisnik završi
|
||||||
func PokreniSetup(fsys fs.FS) {
|
func PokreniSetup(fsys fs.FS, envFajl string) {
|
||||||
port := NadjiSlobodanPort()
|
port := NadjiSlobodanPort()
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
slog.Error("setup: nije pronađen nijedan slobodan port"); os.Exit(1)
|
slog.Error("setup: nije pronađen nijedan slobodan port"); os.Exit(1)
|
||||||
@@ -90,7 +90,7 @@ func PokreniSetup(fsys fs.FS) {
|
|||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
json.NewDecoder(req.Body).Decode(&telo)
|
json.NewDecoder(req.Body).Decode(&telo)
|
||||||
if err := SacuvajEnv(telo.Port); err != nil {
|
if err := SacuvajEnv(telo.Port, envFajl); err != nil {
|
||||||
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
http.Error(w, "Greška pri čuvanju podešavanja", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type podaciAdminProfil struct {
|
|||||||
BrojRezervnih int // koliko neiskorišćenih rezervnih kodova je preostalo
|
BrojRezervnih int // koliko neiskorišćenih rezervnih kodova je preostalo
|
||||||
LokalnaTema string
|
LokalnaTema string
|
||||||
KoristiLokalnuTemu bool
|
KoristiLokalnuTemu bool
|
||||||
|
JelDemo bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type podaciProfilTema struct {
|
type podaciProfilTema struct {
|
||||||
@@ -312,6 +313,7 @@ func (h *Handler) AdminProfil(w http.ResponseWriter, r *http.Request) {
|
|||||||
BrojRezervnih: brojRezervnih,
|
BrojRezervnih: brojRezervnih,
|
||||||
LokalnaTema: svezi.LokalnaTema,
|
LokalnaTema: svezi.LokalnaTema,
|
||||||
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
|
KoristiLokalnuTemu: svezi.KoristiLokalnuTemu,
|
||||||
|
JelDemo: h.JelDemo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,6 +354,7 @@ func (h *Handler) generisiIPrikaziKodove(w http.ResponseWriter, r *http.Request,
|
|||||||
TotpAktivan: true,
|
TotpAktivan: true,
|
||||||
RezervniKodovi: kodovi,
|
RezervniKodovi: kodovi,
|
||||||
BrojRezervnih: len(kodovi),
|
BrojRezervnih: len(kodovi),
|
||||||
|
JelDemo: h.JelDemo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +366,12 @@ func (h *Handler) AdminPromeniLozinku(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.JelDemo {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Promena lozinke nije dozvoljena u demo modu.")
|
||||||
|
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
|
middleware.SetFlash(w, r, h.DB, "greska", "Greška. Pokušajte ponovo.")
|
||||||
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||||
@@ -409,6 +418,12 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if h.JelDemo {
|
||||||
|
middleware.SetFlash(w, r, h.DB, "greska", "Podešavanje 2FA nije dostupno u demo modu.")
|
||||||
|
http.Redirect(w, r, "/admin/profil", http.StatusSeeOther)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
podesavanja, _ := sqlite.DohvatiSvaPodesavanja(r.Context(), h.DB)
|
||||||
totp, err := auth.GenerisuTotpTajnu(k.KorisnickoIme, podesavanja["naziv_firme"])
|
totp, err := auth.GenerisuTotpTajnu(k.KorisnickoIme, podesavanja["naziv_firme"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -426,6 +441,7 @@ func (h *Handler) AdminTotpPokreni(w http.ResponseWriter, r *http.Request) {
|
|||||||
TotpURI: totp.URI,
|
TotpURI: totp.URI,
|
||||||
TotpTajna: totp.Tajna,
|
TotpTajna: totp.Tajna,
|
||||||
TotpQR: template.URL("data:image/png;base64," + totp.QRBase64),
|
TotpQR: template.URL("data:image/png;base64," + totp.QRBase64),
|
||||||
|
JelDemo: h.JelDemo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,6 +481,7 @@ func (h *Handler) AdminTotpAktivacija(w http.ResponseWriter, r *http.Request) {
|
|||||||
TotpTajna: tajna,
|
TotpTajna: tajna,
|
||||||
TotpQR: template.URL("data:image/png;base64," + qr),
|
TotpQR: template.URL("data:image/png;base64," + qr),
|
||||||
Greska: "totp",
|
Greska: "totp",
|
||||||
|
JelDemo: h.JelDemo,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: os.Getenv("NTECH_ENV") == "production",
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type Handler struct {
|
|||||||
PdvKprRepo db.PdvKprRepository
|
PdvKprRepo db.PdvKprRepository
|
||||||
NivelacijaRepo db.NivelacijaRepository
|
NivelacijaRepo db.NivelacijaRepository
|
||||||
Verzija string
|
Verzija string
|
||||||
|
JelDemo bool
|
||||||
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
AssetV string // verzija statičkih fajlova za cache-busting (postavlja se pri pokretanju)
|
||||||
Templates map[string]*template.Template
|
Templates map[string]*template.Template
|
||||||
TemplatesFS fs.FS
|
TemplatesFS fs.FS
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func (h *Handler) PrikazPrijave(w http.ResponseWriter, r *http.Request) {
|
|||||||
"LoginPozadinaBlurKartice": loginBlurKartice,
|
"LoginPozadinaBlurKartice": loginBlurKartice,
|
||||||
"LoginPozadinaZatamnjenjeKartice": loginZatamnjenjeKartice,
|
"LoginPozadinaZatamnjenjeKartice": loginZatamnjenjeKartice,
|
||||||
"Verzija": h.Verzija,
|
"Verzija": h.Verzija,
|
||||||
|
"JelDemo": h.JelDemo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,7 +269,7 @@ func (h *Handler) Odjava(w http.ResponseWriter, r *http.Request) {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Expires: time.Unix(0, 0),
|
Expires: time.Unix(0, 0),
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Secure: os.Getenv("NTECH_ENV") == "production",
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
})
|
})
|
||||||
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||||
@@ -317,7 +318,7 @@ func napraviKolacic(token string, istice time.Time) *http.Cookie {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Expires: istice,
|
Expires: istice,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: os.Getenv("NTECH_ENV") == "production",
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func RequireAuth(db *sql.DB, totpKljuc []byte) func(http.Handler) http.Handler {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
Expires: time.Unix(0, 0),
|
Expires: time.Unix(0, 0),
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Secure: os.Getenv("NTECH_ENV") == "production",
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
})
|
})
|
||||||
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
http.Redirect(w, r, "/prijava", http.StatusSeeOther)
|
||||||
@@ -157,7 +157,7 @@ func postaviFlashGresku(w http.ResponseWriter, poruka string) {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: 60,
|
MaxAge: 60,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: os.Getenv("NTECH_ENV") == "production",
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func CsrfMiddleware(next http.Handler) http.Handler {
|
|||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: 86400 * 30,
|
MaxAge: 86400 * 30,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Secure: os.Getenv("NTECH_ENV") == "production",
|
Secure: os.Getenv("NTECH_ENV") == "production" || os.Getenv("NTECH_ENV") == "demo",
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
GITEA_IMAGE="git.vm-net.in.rs/dasko/ntech"
|
||||||
|
GITHUB_IMAGE="ghcr.io/dalibor31/ntech"
|
||||||
|
VER_FAJL="VERSION"
|
||||||
|
|
||||||
|
clear
|
||||||
|
echo "╔══════════════════════════════════════╗"
|
||||||
|
echo "║ NTech — Build alat ║"
|
||||||
|
echo "╚══════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Verzija
|
||||||
|
VERZIJA_DEFAULT=$(cat "$VER_FAJL" 2>/dev/null || echo "0.0.0")
|
||||||
|
read -p "1) Verzija [${VERZIJA_DEFAULT}]: " VERZIJA
|
||||||
|
VERZIJA="${VERZIJA:-$VERZIJA_DEFAULT}"
|
||||||
|
if [ "$VERZIJA" != "$VERZIJA_DEFAULT" ]; then
|
||||||
|
echo "$VERZIJA" > "$VER_FAJL"
|
||||||
|
echo " → VERSION ažuriran na: $VERZIJA"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 2. Okruženje
|
||||||
|
echo "2) Okruženje:"
|
||||||
|
echo " 1) Production (podrazumevano)"
|
||||||
|
echo " 2) Development"
|
||||||
|
read -p " Izbor [1/2]: " OKR_IZBOR
|
||||||
|
OKR_IZBOR="${OKR_IZBOR:-1}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 3. Platforma
|
||||||
|
echo "3) Platforma:"
|
||||||
|
echo " 1) Linux (podrazumevano)"
|
||||||
|
echo " 2) Windows"
|
||||||
|
echo " 3) Obe"
|
||||||
|
read -p " Izbor [1/2/3]: " PLATFORMA_IZBOR
|
||||||
|
PLATFORMA_IZBOR="${PLATFORMA_IZBOR:-1}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 4. UPX
|
||||||
|
read -p "4) Kompresovati UPX-om? [d/N]: " UPX_IZBOR
|
||||||
|
UPX_IZBOR="${UPX_IZBOR:-n}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 5. Build
|
||||||
|
read -p "5) Pokrenuti build? [D/n]: " BUILD_IZBOR
|
||||||
|
BUILD_IZBOR="${BUILD_IZBOR:-d}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 6. Docker push
|
||||||
|
read -p "6) Push Docker image (Gitea + GitHub)? [d/N]: " DOCKER_IZBOR
|
||||||
|
DOCKER_IZBOR="${DOCKER_IZBOR:-n}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Izračunaj vrednosti ──────────────────────────
|
||||||
|
if [ "$OKR_IZBOR" = "2" ]; then
|
||||||
|
OKRUZENJE="development"
|
||||||
|
VERZIJA_BUILD="dev-${VERZIJA}"
|
||||||
|
LDFLAGS="-X main.Verzija=dev-${VERZIJA}"
|
||||||
|
TRIMPATH=""
|
||||||
|
else
|
||||||
|
OKRUZENJE="production"
|
||||||
|
VERZIJA_BUILD="${VERZIJA}"
|
||||||
|
LDFLAGS="-X main.Verzija=${VERZIJA} -s -w"
|
||||||
|
TRIMPATH="-trimpath"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$PLATFORMA_IZBOR" in
|
||||||
|
2) PLATFORMA_NAZIV="Windows" ;;
|
||||||
|
3) PLATFORMA_NAZIV="Linux + Windows" ;;
|
||||||
|
*) PLATFORMA_NAZIV="Linux" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$UPX_IZBOR" =~ ^[dDyY] ]]; then UPX_NAZIV="da"; else UPX_NAZIV="ne"; fi
|
||||||
|
if [[ "$BUILD_IZBOR" =~ ^[dDyY] ]]; then BUILD_NAZIV="da"; else BUILD_NAZIV="ne"; fi
|
||||||
|
if [[ "$DOCKER_IZBOR" =~ ^[dDyY] ]]; then DOCKER_NAZIV="da"; else DOCKER_NAZIV="ne"; fi
|
||||||
|
|
||||||
|
# ── Sažetak ──────────────────────────────────────
|
||||||
|
echo "──────────────────────────────────────────"
|
||||||
|
echo " Verzija : ${VERZIJA_BUILD}"
|
||||||
|
echo " Okruženje : ${OKRUZENJE}"
|
||||||
|
echo " Platforma : ${PLATFORMA_NAZIV}"
|
||||||
|
echo " UPX : ${UPX_NAZIV}"
|
||||||
|
echo " Build : ${BUILD_NAZIV}"
|
||||||
|
echo " Docker : ${DOCKER_NAZIV}"
|
||||||
|
echo "──────────────────────────────────────────"
|
||||||
|
echo ""
|
||||||
|
read -p "Pokrenuti? [D/n]: " POTVRDA
|
||||||
|
POTVRDA="${POTVRDA:-d}"
|
||||||
|
if [[ ! "$POTVRDA" =~ ^[dDyY] ]]; then
|
||||||
|
echo "Otkazano."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── UPX: instaliraj ako treba ────────────────────
|
||||||
|
if [[ "$UPX_IZBOR" =~ ^[dDyY] ]] && [[ "$BUILD_IZBOR" =~ ^[dDyY] ]]; then
|
||||||
|
if ! command -v upx &>/dev/null; then
|
||||||
|
echo "→ UPX nije instaliran. Instaliram..."
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
sudo apt-get install -y upx
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
sudo dnf install -y upx
|
||||||
|
elif command -v pacman &>/dev/null; then
|
||||||
|
sudo pacman -S --noconfirm upx
|
||||||
|
elif command -v brew &>/dev/null; then
|
||||||
|
brew install upx
|
||||||
|
else
|
||||||
|
echo " UPOZORENJE: Ne mogu da instaliram UPX — nepoznat menadžer paketa. Kompresija preskočena."
|
||||||
|
UPX_IZBOR="n"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Build funkcija ───────────────────────────────
|
||||||
|
build_za() {
|
||||||
|
local GOOS_VAL="$1"
|
||||||
|
local NAZIV="$2"
|
||||||
|
echo "→ Build ${GOOS_VAL}/amd64: ${NAZIV}"
|
||||||
|
CGO_ENABLED=0 GOOS="${GOOS_VAL}" GOARCH=amd64 go build \
|
||||||
|
-ldflags "${LDFLAGS}" \
|
||||||
|
${TRIMPATH} \
|
||||||
|
-o "${NAZIV}" \
|
||||||
|
./cmd/ntech
|
||||||
|
ls -lh "${NAZIV}"
|
||||||
|
|
||||||
|
if [[ "$UPX_IZBOR" =~ ^[dDyY] ]] && command -v upx &>/dev/null; then
|
||||||
|
echo " Kompresovanje sa UPX..."
|
||||||
|
upx --best "${NAZIV}"
|
||||||
|
ls -lh "${NAZIV}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 5. Build ─────────────────────────────────────
|
||||||
|
if [[ "$BUILD_IZBOR" =~ ^[dDyY] ]]; then
|
||||||
|
echo "=== Build ==="
|
||||||
|
case "$PLATFORMA_IZBOR" in
|
||||||
|
2)
|
||||||
|
build_za "windows" "ntech.exe"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
build_za "linux" "ntech" &
|
||||||
|
PID_LINUX=$!
|
||||||
|
build_za "windows" "ntech.exe" &
|
||||||
|
PID_WIN=$!
|
||||||
|
wait $PID_LINUX $PID_WIN
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
build_za "linux" "ntech"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6. Docker push ───────────────────────────────
|
||||||
|
if [[ "$DOCKER_IZBOR" =~ ^[dDyY] ]]; then
|
||||||
|
echo "=== Docker ==="
|
||||||
|
|
||||||
|
# Ako Linux binary nije izgrađen u ovom pozivu (build=ne ili platforma=Windows), izgradi ga
|
||||||
|
LINUX_VEC_IZGRADJEN=0
|
||||||
|
if [[ "$BUILD_IZBOR" =~ ^[dDyY] ]] && [ "$PLATFORMA_IZBOR" != "2" ]; then
|
||||||
|
LINUX_VEC_IZGRADJEN=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LINUX_VEC_IZGRADJEN" = "0" ]; then
|
||||||
|
echo "→ Gradim Linux binary za Docker (sa UPX ako je uključen)..."
|
||||||
|
# instaliraj UPX ako treba, a još nije instaliran
|
||||||
|
if [[ "$UPX_IZBOR" =~ ^[dDyY] ]] && ! command -v upx &>/dev/null; then
|
||||||
|
echo "→ UPX nije instaliran. Instaliram..."
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
sudo apt-get install -y upx
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
sudo dnf install -y upx
|
||||||
|
elif command -v pacman &>/dev/null; then
|
||||||
|
sudo pacman -S --noconfirm upx
|
||||||
|
elif command -v brew &>/dev/null; then
|
||||||
|
brew install upx
|
||||||
|
else
|
||||||
|
echo " UPOZORENJE: Ne mogu da instaliram UPX — kompresija preskočena."
|
||||||
|
UPX_IZBOR="n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
build_za "linux" "ntech"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Build Docker image..."
|
||||||
|
docker build --build-arg="VERZIJA=${VERZIJA}" \
|
||||||
|
-t "${GITEA_IMAGE}:${VERZIJA}" \
|
||||||
|
-t "${GITEA_IMAGE}:latest" \
|
||||||
|
-t "${GITHUB_IMAGE}:${VERZIJA}" \
|
||||||
|
-t "${GITHUB_IMAGE}:latest" \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo "→ Push na Gitea..."
|
||||||
|
docker push "${GITEA_IMAGE}:${VERZIJA}"
|
||||||
|
docker push "${GITEA_IMAGE}:latest"
|
||||||
|
|
||||||
|
echo "→ Push na GitHub..."
|
||||||
|
docker push "${GITHUB_IMAGE}:${VERZIJA}"
|
||||||
|
docker push "${GITHUB_IMAGE}:latest"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Gotovo! NTech v${VERZIJA_BUILD}"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg width="100%" viewBox="0 0 750 690" role="img" style="" xmlns="http://www.w3.org/2000/svg">
|
<svg viewBox="0 0 750 690" role="img" style="" xmlns="http://www.w3.org/2000/svg">
|
||||||
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">NTech favicon v4</title>
|
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">NTech favicon v4</title>
|
||||||
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Moderna ikonica sa NT slovima i tehnološkim akcentom</desc>
|
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Moderna ikonica sa NT slovima i tehnološkim akcentom</desc>
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
@@ -5,6 +5,9 @@
|
|||||||
<div class="kartica animiraj">
|
<div class="kartica animiraj">
|
||||||
<div style="font-size: 15px; font-weight: 500; color: var(--tekst-glavni); margin-bottom: 16px; padding-bottom: 12px; border-bottom: 0.5px solid var(--ivica)">Promena lozinke</div>
|
<div style="font-size: 15px; font-weight: 500; color: var(--tekst-glavni); margin-bottom: 16px; padding-bottom: 12px; border-bottom: 0.5px solid var(--ivica)">Promena lozinke</div>
|
||||||
|
|
||||||
|
{{if .JelDemo}}
|
||||||
|
<p style="font-size: 13px; color: var(--tekst-sporedni)">Promena lozinke nije dostupna u demo modu.</p>
|
||||||
|
{{else}}
|
||||||
<form method="POST" action="/admin/profil/lozinka">
|
<form method="POST" action="/admin/profil/lozinka">
|
||||||
<div style="display: flex; flex-direction: column; gap: 12px">
|
<div style="display: flex; flex-direction: column; gap: 12px">
|
||||||
<div>
|
<div>
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TOTP / 2FA -->
|
<!-- TOTP / 2FA -->
|
||||||
@@ -93,9 +97,11 @@
|
|||||||
Status:
|
Status:
|
||||||
<strong>Isključena</strong>
|
<strong>Isključena</strong>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size: 13px; color: var(--tekst-sporedni)">Preporučujemo uključivanje dvostepene verifikacije.</div>
|
<div style="font-size: 13px; color: var(--tekst-sporedni)">
|
||||||
|
{{if .JelDemo}}Podešavanje 2FA nije dostupno u demo modu.{{else}}Preporučujemo uključivanje dvostepene verifikacije.{{end}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/admin/profil/totp/pokreni" class="btn-primarno">Podesi 2FA</a>
|
{{if not .JelDemo}}<a href="/admin/profil/totp/pokreni" class="btn-primarno">Podesi 2FA</a>{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -57,11 +57,11 @@
|
|||||||
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
<input type="hidden" name="_csrf" value="{{.CsrfToken}}">
|
||||||
<div class="polje">
|
<div class="polje">
|
||||||
<label for="korisnicko_ime">Korisničko ime</label>
|
<label for="korisnicko_ime">Korisničko ime</label>
|
||||||
<input type="text" id="korisnicko_ime" name="korisnicko_ime" autocomplete="username" autofocus required>
|
<input type="text" id="korisnicko_ime" name="korisnicko_ime" autocomplete="username" autofocus required{{if .JelDemo}} value="Demo"{{end}}>
|
||||||
</div>
|
</div>
|
||||||
<div class="polje">
|
<div class="polje">
|
||||||
<label for="lozinka">Lozinka</label>
|
<label for="lozinka">Lozinka</label>
|
||||||
<input type="password" id="lozinka" name="lozinka" autocomplete="current-password" required>
|
<input type="password" id="lozinka" name="lozinka" autocomplete="current-password" required{{if .JelDemo}} value="Demo1234"{{end}}>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="dugme">Prijavi se</button>
|
<button type="submit" class="dugme">Prijavi se</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
.topbar { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border-bottom: 1px solid rgba(255,255,255,0.12) !important; }
|
.topbar { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border-bottom: 1px solid rgba(255,255,255,0.12) !important; }
|
||||||
.topbar-naslov { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
.topbar-naslov { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||||
.kartica, .premesti-modal { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border: 1px solid rgba(255,255,255,0.12) !important; }
|
.kartica, .premesti-modal { background: rgba(0,0,0,{{if .AppPozadinaGlassOpacity}}{{.AppPozadinaGlassOpacity}}%{{else}}0.08{{end}}) !important; backdrop-filter: blur({{.AppPozadinaBlur}}px); -webkit-backdrop-filter: blur({{.AppPozadinaBlur}}px); border: 1px solid rgba(255,255,255,0.12) !important; }
|
||||||
|
body:not([data-hover]) .kartica:hover { background: rgba(0,0,0,0.38) !important; }
|
||||||
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
.kartica p, .kartica span, .kartica h1, .kartica h2, .kartica h3, .kartica h4, .kartica label, .kartica td, .kartica th, .kartica li, .kartica a { color: rgba(255,255,255,0.95) !important; text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||||
/* naslovi kartica i labele su goli <div> (boju im NE diramo, da namerno obojeni — crveni/akcenat — ostanu) — samo senka da se vide */
|
/* naslovi kartica i labele su goli <div> (boju im NE diramo, da namerno obojeni — crveni/akcenat — ostanu) — samo senka da se vide */
|
||||||
.kartica div { text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
.kartica div { text-shadow: 0 1px 2px rgba(0,0,0,0.9), 0 0 6px rgba(0,0,0,0.55); }
|
||||||
|
|||||||
Reference in New Issue
Block a user