Compare commits
No commits in common. "74d52e05c1c15676a0c945509673ec50243e30eb" and "318e6e00fc71754652bb1dee6ed7e5bb4a8973d0" have entirely different histories.
74d52e05c1
...
318e6e00fc
17
TESTING.md
17
TESTING.md
@ -1,8 +1,8 @@
|
||||
# DAL License Server — Test Checklista
|
||||
|
||||
## Ukupno testova: 182
|
||||
## Ukupno testova: 179
|
||||
- Go unit testovi: 46
|
||||
- Playwright E2E testovi: 136
|
||||
- Playwright E2E testovi: 133
|
||||
|
||||
## Pokretanje testova
|
||||
|
||||
@ -92,12 +92,11 @@ go test ./internal/... -v -count=1 && npx playwright test
|
||||
|
||||
## Playwright E2E Testovi (133)
|
||||
|
||||
### Login stranica (18 testova)
|
||||
- [x] Prikazuje login formu sa svim elementima (username + password)
|
||||
- [x] Username polje ima autofocus
|
||||
### Login stranica (15 testova)
|
||||
- [x] Prikazuje login formu sa svim elementima
|
||||
- [x] Password polje ima autofocus
|
||||
- [x] Forma ima ispravnu action i method
|
||||
- [x] Prijava sa ispravnim username i password preusmerava na dashboard
|
||||
- [x] Prijava sa pogresnim username-om prikazuje gresku
|
||||
- [x] Prijava sa ispravnom lozinkom preusmerava na dashboard
|
||||
- [x] Prijava sa pogresnom lozinkom prikazuje gresku
|
||||
- [x] Prijava sa praznom lozinkom (browser validacija)
|
||||
- [x] Razlicite pogresne lozinke (7 pokusaja)
|
||||
@ -109,8 +108,6 @@ go test ./internal/... -v -count=1 && npx playwright test
|
||||
- [x] CSS je ucitan
|
||||
- [x] Ispravan page title
|
||||
- [x] Visestruki logini kreiraju razlicite sesije
|
||||
- [x] Navbar prikazuje ime korisnika posle logina
|
||||
- [x] Username i password labele su prikazane
|
||||
|
||||
### Dashboard stranica (18 testova)
|
||||
- [x] Prikazuje naslov Dashboard
|
||||
@ -258,4 +255,4 @@ go test ./internal/... -v -count=1 && npx playwright test
|
||||
|
||||
---
|
||||
|
||||
*Poslednje azuriranje: 04.03.2026 — 182 testova (46 Go + 136 Playwright)*
|
||||
*Poslednje azuriranje: 04.03.2026 — 179 testova (46 Go + 133 Playwright)*
|
||||
|
||||
@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"dal-license-server/internal/config"
|
||||
"dal-license-server/internal/handler"
|
||||
"dal-license-server/internal/model"
|
||||
"dal-license-server/internal/repository"
|
||||
"dal-license-server/internal/router"
|
||||
"dal-license-server/internal/service"
|
||||
@ -15,7 +14,6 @@ import (
|
||||
"strconv"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -45,10 +43,6 @@ func main() {
|
||||
licenseRepo := repository.NewLicenseRepo(db)
|
||||
activationRepo := repository.NewActivationRepo(db)
|
||||
auditRepo := repository.NewAuditRepo(db)
|
||||
userRepo := repository.NewUserRepo(db)
|
||||
|
||||
// Seed default admin user
|
||||
seedDefaultAdmin(userRepo, cfg.AdminPassword)
|
||||
|
||||
// Services
|
||||
licenseSvc := service.NewLicenseService(licenseRepo, auditRepo)
|
||||
@ -57,7 +51,7 @@ func main() {
|
||||
// Handlers
|
||||
clientHandler := handler.NewClientHandler(activationSvc)
|
||||
adminHandler := handler.NewAdminHandler(licenseSvc, activationSvc, auditRepo)
|
||||
dashboardHandler := handler.NewDashboardHandler(licenseSvc, activationSvc, auditRepo, userRepo, "templates")
|
||||
dashboardHandler := handler.NewDashboardHandler(licenseSvc, activationSvc, auditRepo, "templates", cfg.AdminPassword)
|
||||
|
||||
// Rate limits
|
||||
rlActivate, _ := strconv.Atoi(cfg.RateLimitActivate)
|
||||
@ -80,11 +74,7 @@ func main() {
|
||||
}
|
||||
|
||||
func runMigrations(db *sql.DB) {
|
||||
files := []string{
|
||||
"migrations/001_create_tables.sql",
|
||||
"migrations/002_seed_products.sql",
|
||||
"migrations/003_create_admin_users.sql",
|
||||
}
|
||||
files := []string{"migrations/001_create_tables.sql", "migrations/002_seed_products.sql"}
|
||||
for _, f := range files {
|
||||
data, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
@ -97,31 +87,3 @@ func runMigrations(db *sql.DB) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func seedDefaultAdmin(userRepo *repository.UserRepo, adminPassword string) {
|
||||
count, err := userRepo.Count()
|
||||
if err != nil {
|
||||
log.Printf("Seed admin: count error: %v", err)
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(adminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Fatal("Seed admin: bcrypt error: ", err)
|
||||
}
|
||||
|
||||
user := &model.AdminUser{
|
||||
Username: "admin",
|
||||
PasswordHash: string(hash),
|
||||
FullName: "Administrator",
|
||||
Active: true,
|
||||
}
|
||||
if err := userRepo.Create(user); err != nil {
|
||||
log.Printf("Seed admin: %v (may already exist)", err)
|
||||
} else {
|
||||
log.Println("Default admin korisnik kreiran (username: admin)")
|
||||
}
|
||||
}
|
||||
|
||||
7
go.mod
7
go.mod
@ -1,10 +1,7 @@
|
||||
module dal-license-server
|
||||
|
||||
go 1.24.0
|
||||
go 1.23.6
|
||||
|
||||
require github.com/go-sql-driver/mysql v1.9.3
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
)
|
||||
require filippo.io/edwards25519 v1.1.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@ -2,5 +2,3 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
|
||||
@ -15,28 +15,19 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type SessionData struct {
|
||||
UserID int64
|
||||
Username string
|
||||
FullName string
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type DashboardHandler struct {
|
||||
licenses *service.LicenseService
|
||||
activation *service.ActivationService
|
||||
audit *repository.AuditRepo
|
||||
userRepo *repository.UserRepo
|
||||
templates map[string]*template.Template
|
||||
sessions map[string]*SessionData
|
||||
sessions map[string]time.Time
|
||||
mu sync.RWMutex
|
||||
password string
|
||||
}
|
||||
|
||||
func NewDashboardHandler(licenses *service.LicenseService, activation *service.ActivationService, audit *repository.AuditRepo, userRepo *repository.UserRepo, tmplDir string) *DashboardHandler {
|
||||
func NewDashboardHandler(licenses *service.LicenseService, activation *service.ActivationService, audit *repository.AuditRepo, tmplDir, password string) *DashboardHandler {
|
||||
funcMap := template.FuncMap{
|
||||
"formatDate": func(t time.Time) string { return t.Format("02.01.2006 15:04") },
|
||||
"formatDateShort": func(t time.Time) string { return t.Format("02.01.2006") },
|
||||
@ -70,9 +61,9 @@ func NewDashboardHandler(licenses *service.LicenseService, activation *service.A
|
||||
licenses: licenses,
|
||||
activation: activation,
|
||||
audit: audit,
|
||||
userRepo: userRepo,
|
||||
templates: make(map[string]*template.Template),
|
||||
sessions: make(map[string]*SessionData),
|
||||
sessions: make(map[string]time.Time),
|
||||
password: password,
|
||||
}
|
||||
|
||||
layoutFiles, _ := filepath.Glob(filepath.Join(tmplDir, "layout", "*.html"))
|
||||
@ -104,33 +95,23 @@ func (h *DashboardHandler) render(w http.ResponseWriter, name string, data inter
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) renderWithSession(w http.ResponseWriter, r *http.Request, name string, data map[string]interface{}) {
|
||||
if sess := h.getSession(r); sess != nil {
|
||||
data["CurrentUser"] = sess.FullName
|
||||
if data["CurrentUser"] == "" {
|
||||
data["CurrentUser"] = sess.Username
|
||||
}
|
||||
}
|
||||
h.render(w, name, data)
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) getSession(r *http.Request) *SessionData {
|
||||
func (h *DashboardHandler) isLoggedIn(r *http.Request) bool {
|
||||
c, err := r.Cookie("dash_session")
|
||||
if err != nil {
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
sess, ok := h.sessions[c.Value]
|
||||
if !ok || time.Now().After(sess.Expiry) {
|
||||
return nil
|
||||
exp, ok := h.sessions[c.Value]
|
||||
if !ok || time.Now().After(exp) {
|
||||
return false
|
||||
}
|
||||
return sess
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) RequireLogin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if h.getSession(r) == nil {
|
||||
if !h.isLoggedIn(r) {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
@ -143,17 +124,9 @@ func (h *DashboardHandler) LoginPage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (h *DashboardHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
|
||||
user, err := h.userRepo.GetByUsername(username)
|
||||
if err != nil || !user.Active {
|
||||
h.render(w, "login.html", map[string]interface{}{"Error": "Pogresno korisnicko ime ili lozinka"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
|
||||
h.render(w, "login.html", map[string]interface{}{"Error": "Pogresno korisnicko ime ili lozinka"})
|
||||
if password != h.password {
|
||||
h.render(w, "login.html", map[string]interface{}{"Error": "Pogresna lozinka"})
|
||||
return
|
||||
}
|
||||
|
||||
@ -162,17 +135,9 @@ func (h *DashboardHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
sid := hex.EncodeToString(b)
|
||||
|
||||
h.mu.Lock()
|
||||
h.sessions[sid] = &SessionData{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
FullName: user.FullName,
|
||||
Expiry: time.Now().Add(8 * time.Hour),
|
||||
}
|
||||
h.sessions[sid] = time.Now().Add(8 * time.Hour)
|
||||
h.mu.Unlock()
|
||||
|
||||
h.userRepo.UpdateLastLogin(user.ID)
|
||||
h.audit.Log(nil, "LOGIN", clientIP(r), map[string]string{"username": user.Username})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "dash_session",
|
||||
Value: sid,
|
||||
@ -186,12 +151,8 @@ func (h *DashboardHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *DashboardHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
if c, err := r.Cookie("dash_session"); err == nil {
|
||||
h.mu.Lock()
|
||||
sess := h.sessions[c.Value]
|
||||
delete(h.sessions, c.Value)
|
||||
h.mu.Unlock()
|
||||
if sess != nil {
|
||||
h.audit.Log(nil, "LOGOUT", clientIP(r), map[string]string{"username": sess.Username})
|
||||
}
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: "dash_session", MaxAge: -1, Path: "/"})
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
@ -202,7 +163,7 @@ func (h *DashboardHandler) Dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
expiring, _ := h.licenses.ExpiringIn(7)
|
||||
recent, _ := h.audit.Recent(10)
|
||||
|
||||
h.renderWithSession(w, r, "dashboard.html", map[string]interface{}{
|
||||
h.render(w, "dashboard.html", map[string]interface{}{
|
||||
"Stats": stats,
|
||||
"Expiring": expiring,
|
||||
"Recent": recent,
|
||||
@ -218,7 +179,7 @@ func (h *DashboardHandler) LicenseList(w http.ResponseWriter, r *http.Request) {
|
||||
licenses, _ := h.licenses.List(product, status, search)
|
||||
products, _ := h.licenses.GetProducts()
|
||||
|
||||
h.renderWithSession(w, r, "licenses.html", map[string]interface{}{
|
||||
h.render(w, "licenses.html", map[string]interface{}{
|
||||
"Licenses": licenses,
|
||||
"Products": products,
|
||||
"Product": product,
|
||||
@ -230,7 +191,7 @@ func (h *DashboardHandler) LicenseList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (h *DashboardHandler) LicenseNew(w http.ResponseWriter, r *http.Request) {
|
||||
products, _ := h.licenses.GetProducts()
|
||||
h.renderWithSession(w, r, "license-new.html", map[string]interface{}{
|
||||
h.render(w, "license-new.html", map[string]interface{}{
|
||||
"Products": products,
|
||||
"ActivePage": "licenses",
|
||||
})
|
||||
@ -270,7 +231,7 @@ func (h *DashboardHandler) LicenseCreate(w http.ResponseWriter, r *http.Request)
|
||||
license, err := h.licenses.Create(req, clientIP(r))
|
||||
if err != nil {
|
||||
products, _ := h.licenses.GetProducts()
|
||||
h.renderWithSession(w, r, "license-new.html", map[string]interface{}{
|
||||
h.render(w, "license-new.html", map[string]interface{}{
|
||||
"Products": products,
|
||||
"Error": err.Error(),
|
||||
"ActivePage": "licenses",
|
||||
@ -292,7 +253,7 @@ func (h *DashboardHandler) LicenseDetail(w http.ResponseWriter, r *http.Request)
|
||||
activations, _ := h.activation.ListByLicense(id)
|
||||
auditEntries, _ := h.audit.List(&id, 20)
|
||||
|
||||
h.renderWithSession(w, r, "license-detail.html", map[string]interface{}{
|
||||
h.render(w, "license-detail.html", map[string]interface{}{
|
||||
"License": license,
|
||||
"Activations": activations,
|
||||
"Audit": auditEntries,
|
||||
@ -315,7 +276,7 @@ func (h *DashboardHandler) LicenseRelease(w http.ResponseWriter, r *http.Request
|
||||
|
||||
func (h *DashboardHandler) AuditPage(w http.ResponseWriter, r *http.Request) {
|
||||
entries, _ := h.audit.Recent(100)
|
||||
h.renderWithSession(w, r, "audit.html", map[string]interface{}{
|
||||
h.render(w, "audit.html", map[string]interface{}{
|
||||
"Entries": entries,
|
||||
"ActivePage": "audit",
|
||||
})
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdminUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"-"`
|
||||
FullName string `json:"full_name"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
LastLoginAt sql.NullTime `json:"last_login_at"`
|
||||
}
|
||||
@ -1,53 +0,0 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"dal-license-server/internal/model"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type UserRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewUserRepo(db *sql.DB) *UserRepo {
|
||||
return &UserRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepo) GetByUsername(username string) (*model.AdminUser, error) {
|
||||
var u model.AdminUser
|
||||
err := r.db.QueryRow(
|
||||
"SELECT id, username, password_hash, full_name, active, created_at, updated_at, last_login_at FROM admin_users WHERE username = ?",
|
||||
username,
|
||||
).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.FullName, &u.Active, &u.CreatedAt, &u.UpdatedAt, &u.LastLoginAt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get user by username: %w", err)
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) Create(u *model.AdminUser) error {
|
||||
res, err := r.db.Exec(
|
||||
"INSERT INTO admin_users (username, password_hash, full_name, active) VALUES (?, ?, ?, ?)",
|
||||
u.Username, u.PasswordHash, u.FullName, u.Active,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create user: %w", err)
|
||||
}
|
||||
u.ID, _ = res.LastInsertId()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) UpdateLastLogin(id int64) error {
|
||||
_, err := r.db.Exec("UPDATE admin_users SET last_login_at = NOW() WHERE id = ?", id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update last login: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserRepo) Count() (int, error) {
|
||||
var count int
|
||||
err := r.db.QueryRow("SELECT COUNT(*) FROM admin_users").Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP NULL
|
||||
);
|
||||
@ -2,8 +2,7 @@
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
||||
.navbar { background: #1a1a2e; color: #fff; padding: 0.75rem 2rem; display: flex; align-items: center; gap: 2rem; }
|
||||
.nav-brand { font-size: 1.2rem; font-weight: 700; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.nav-logo { width: 28px; height: 28px; }
|
||||
.nav-brand { font-size: 1.2rem; font-weight: 700; }
|
||||
.nav-links { display: flex; gap: 1rem; flex: 1; }
|
||||
.nav-links a { color: #aaa; text-decoration: none; padding: 0.5rem 1rem; border-radius: 4px; }
|
||||
.nav-links a:hover, .nav-links a.active { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
@ -49,8 +48,7 @@ code { background: #f1f5f9; padding: 0.15rem 0.5rem; border-radius: 3px; font-si
|
||||
.stat-row { display: flex; justify-content: space-between; padding: 0.2rem 0; font-size: 0.9rem; }
|
||||
|
||||
.login-container { max-width: 400px; margin: 100px auto; padding: 2rem; }
|
||||
.login-container h1 { text-align: center; margin-bottom: 2rem; display: flex; align-items: center; justify-content: center; gap: 0.6rem; }
|
||||
.login-logo { width: 32px; height: 32px; }
|
||||
.login-container h1 { text-align: center; margin-bottom: 2rem; }
|
||||
.login-form { background: #fff; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
|
||||
.form-group { margin-bottom: 1rem; }
|
||||
.form-group label { display: block; margin-bottom: 0.4rem; font-weight: 600; font-size: 0.9rem; }
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16 2L4 8v8c0 7.2 5.12 13.92 12 16 6.88-2.08 12-8.8 12-16V8L16 2z" fill="#1a1a2e"/>
|
||||
<path d="M16 5L7 9.5v6.5c0 5.8 4 11.2 9 13 5-1.8 9-7.2 9-13V9.5L16 5z" fill="#2563eb"/>
|
||||
<circle cx="16" cy="13" r="3.5" fill="none" stroke="#fff" stroke-width="1.8"/>
|
||||
<rect x="15" y="16.5" width="2" height="7" rx="1" fill="#fff"/>
|
||||
<rect x="17" y="19.5" width="3" height="1.5" rx=".75" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 482 B |
@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<!-- Shield -->
|
||||
<path d="M32 4L8 16v16c0 14.4 10.24 27.84 24 32 13.76-4.16 24-17.6 24-32V16L32 4z" fill="#1a1a2e"/>
|
||||
<path d="M32 8L12 18v14c0 12.4 8.8 24 20 27.6C43.2 56 52 44.4 52 32V18L32 8z" fill="#2563eb"/>
|
||||
<!-- Key -->
|
||||
<circle cx="32" cy="26" r="7" fill="none" stroke="#fff" stroke-width="2.5"/>
|
||||
<rect x="30.5" y="33" width="3" height="12" rx="1.5" fill="#fff"/>
|
||||
<rect x="33.5" y="39" width="5" height="2.5" rx="1" fill="#fff"/>
|
||||
<rect x="33.5" y="43" width="3.5" height="2.5" rx="1" fill="#fff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 598 B |
@ -4,20 +4,18 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "page-title" .}}DAL License Server{{end}}</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<script src="/static/js/htmx.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand"><img src="/static/img/logo.svg" alt="" class="nav-logo">DAL License Server</div>
|
||||
<div class="nav-brand">DAL License Server</div>
|
||||
<div class="nav-links">
|
||||
<a href="/dashboard" class="{{if eq .ActivePage "dashboard"}}active{{end}}">Dashboard</a>
|
||||
<a href="/licenses" class="{{if eq .ActivePage "licenses"}}active{{end}}">Licence</a>
|
||||
<a href="/audit" class="{{if eq .ActivePage "audit"}}active{{end}}">Audit Log</a>
|
||||
</div>
|
||||
<div class="nav-user">
|
||||
{{if .CurrentUser}}<span class="nav-username">{{.CurrentUser}}</span>{{end}}
|
||||
<form method="POST" action="/logout" style="display:inline">
|
||||
<button type="submit" class="btn btn-sm">Odjava</button>
|
||||
</form>
|
||||
|
||||
@ -4,21 +4,16 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Prijava - DAL License Server</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/img/favicon.svg">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<h1><img src="/static/img/logo.svg" alt="" class="login-logo">DAL License Server</h1>
|
||||
<h1>DAL License Server</h1>
|
||||
<form method="POST" action="/login" class="login-form">
|
||||
{{if .Error}}<div class="alert alert-error">{{.Error}}</div>{{end}}
|
||||
<div class="form-group">
|
||||
<label>Korisnicko ime</label>
|
||||
<input type="text" name="username" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Lozinka</label>
|
||||
<input type="password" name="password" required>
|
||||
<input type="password" name="password" autofocus required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-full">Prijava</button>
|
||||
</form>
|
||||
|
||||
@ -5,7 +5,6 @@ const API_KEY = 'dev-api-key-minimum-32-characters-long';
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
@ -2,7 +2,6 @@ import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
@ -2,7 +2,6 @@ import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
@ -2,7 +2,6 @@ import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
@ -2,7 +2,6 @@ import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
async function login(page: Page) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
|
||||
@ -4,17 +4,16 @@ test.describe('Login stranica', () => {
|
||||
test('prikazuje login formu sa svim elementima', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.locator('h1')).toHaveText('DAL License Server');
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="username"]')).toHaveAttribute('required', '');
|
||||
await expect(page.locator('input[name="password"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"]')).toHaveAttribute('type', 'password');
|
||||
await expect(page.locator('input[name="password"]')).toHaveAttribute('required', '');
|
||||
await expect(page.locator('button[type="submit"]')).toHaveText('Prijava');
|
||||
await expect(page.locator('label')).toHaveText('Lozinka');
|
||||
});
|
||||
|
||||
test('username polje ima autofocus', async ({ page }) => {
|
||||
test('password polje ima autofocus', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.locator('input[name="username"]')).toHaveAttribute('autofocus', '');
|
||||
await expect(page.locator('input[name="password"]')).toHaveAttribute('autofocus', '');
|
||||
});
|
||||
|
||||
test('forma ima ispravnu action i method', async ({ page }) => {
|
||||
@ -24,36 +23,26 @@ test.describe('Login stranica', () => {
|
||||
await expect(form).toHaveAttribute('action', '/login');
|
||||
});
|
||||
|
||||
test('prijava sa ispravnim username i password preusmerava na dashboard', async ({ page }) => {
|
||||
test('prijava sa ispravnom lozinkom preusmerava na dashboard', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page.locator('h1')).toHaveText('Dashboard');
|
||||
});
|
||||
|
||||
test('prijava sa pogresnim username-om prikazuje gresku', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'nepostojeci_user');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator('.alert-error')).toHaveText('Pogresno korisnicko ime ili lozinka');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('prijava sa pogresnom lozinkom prikazuje gresku', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'pogresna_lozinka');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page.locator('.alert-error')).toHaveText('Pogresno korisnicko ime ili lozinka');
|
||||
await expect(page.locator('.alert-error')).toHaveText('Pogresna lozinka');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
// Password polje treba biti prazno posle greske
|
||||
await expect(page.locator('input[name="password"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('prijava sa praznom lozinkom — browser validacija', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
// Ne popunjavamo password — klik na submit
|
||||
await page.click('button[type="submit"]');
|
||||
// Ostajemo na login stranici (browser validation spreci submit)
|
||||
@ -65,7 +54,6 @@ test.describe('Login stranica', () => {
|
||||
for (const lozinka of pogresne) {
|
||||
if (lozinka === '') continue; // skip empty — browser validation
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', lozinka);
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
@ -111,7 +99,6 @@ test.describe('Login stranica', () => {
|
||||
test('visestruki logini kreiraju razlicite sesije', async ({ page, context }) => {
|
||||
// Login prvi put
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
@ -124,7 +111,6 @@ test.describe('Login stranica', () => {
|
||||
await page.click('button:has-text("Odjava")');
|
||||
|
||||
// Login drugi put
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
|
||||
@ -135,22 +121,4 @@ test.describe('Login stranica', () => {
|
||||
// Sesije moraju biti razlicite
|
||||
expect(session1!.value).not.toBe(session2!.value);
|
||||
});
|
||||
|
||||
test('navbar prikazuje ime korisnika posle logina', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'admin123');
|
||||
await page.click('button[type="submit"]');
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page.locator('.nav-username')).toBeVisible();
|
||||
await expect(page.locator('.nav-username')).toHaveText('Administrator');
|
||||
});
|
||||
|
||||
test('username i password labele su prikazane', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
const labels = page.locator('label');
|
||||
const texts = await labels.allTextContents();
|
||||
expect(texts).toContain('Korisnicko ime');
|
||||
expect(texts).toContain('Lozinka');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user