Demo mod, favicon ispravka i putanja ntech.env uz bazu

- NTECH_ENV=demo aktivira demo mod: korisnik Demo/Demo1234 (admin)
  se kreira ili resetuje pri svakom pokretanju
- Login ekran u demo modu prikazuje pre-popunjena polja i "DEMO verzija"
- ntech.env se čuva u istom direktorijumu kao SQLite baza (umesto
  uvek u radnom direktorijumu) — rešava Docker volume problem
- favicon.svg: uklonjen width="100%" koji je sprečavao prikaz ikone u brauzeru
This commit is contained in:
2026-06-19 01:11:40 +02:00
parent 851edb06a4
commit 1cfb44b9a4
7 changed files with 72 additions and 20 deletions
+52 -11
View File
@@ -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,12 @@ 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)
godotenv.Load(envFajl)
podesiLog() podesiLog()
auth.InitAuthLog() auth.InitAuthLog()
@@ -70,8 +76,8 @@ func main() {
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 +86,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 +112,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)
@@ -130,6 +131,13 @@ func main() {
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)
@@ -382,7 +390,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 +408,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,6 +421,39 @@ 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) {
+13 -4
View File
@@ -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)
} }
+2 -2
View File
@@ -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
} }
+1
View File
@@ -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
+1
View File
@@ -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,
}) })
} }
+1 -1
View File
@@ -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:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, 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:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, 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:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, 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:&quot;Inter&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, 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

+2 -2
View File
@@ -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>