diff --git a/go.mod b/go.mod index d78d53e..c1eec60 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,14 @@ module git.send.nrw/sendnrw/hikos go 1.23.1 + +require github.com/go-sql-driver/mysql v1.9.2 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-ldap/ldap/v3 v3.4.11 // indirect + github.com/google/uuid v1.6.0 // indirect + golang.org/x/crypto v0.36.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1d58fe9 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= +github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= +github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= diff --git a/main.go b/main.go index 70c1576..37bfdd4 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,15 @@ package main import ( + "context" + "crypto/rand" + "crypto/tls" + "database/sql" + "encoding/base64" + "errors" "fmt" "html/template" + "log" "net/http" "os" "os/signal" @@ -10,6 +17,9 @@ import ( "strings" "syscall" "time" + + "github.com/go-ldap/ldap/v3" + _ "github.com/go-sql-driver/mysql" ) func getenv(k, d string) string { @@ -34,6 +44,85 @@ func cacheControl(next http.Handler) http.Handler { }) } +/* ################################################################## */ +/* START DER STRUKTUREN */ +/* ################################################################## */ + +type RuleSet struct { + Id int + Name string + Default_contact_read int + Default_contact_write int + Default_contact_delete int + Default_keyword_read int + Default_keyword_write int + Default_keyword_delete int + Default_keyword_attach int + Default_keyword_detach int + Default_aduser_read int + Default_aduser_write int + Default_aduser_delete int + Default_location_read int + Default_location_write int + Default_location_delete int + Default_department_read int + Default_department_write int + Default_department_delete int + Self_contact_read int + Self_contact_write int + Self_keyword_attach int + Self_keyword_detach int + Private_contact_read int + Private_contact_write int + Private_keyword_add int + Private_keyword_delete int + Private_keyword_attach int + Private_keyword_detach int +} + +type ADUser struct { + Id int + SamAccountName string + Sid string + RuleSetId RuleSet +} + +type Department struct { + Id int + Name string +} + +type Location struct { + Id int + Name string + Address string + Zip string + City string +} + +type Contact struct { + Id int + OwnerId int + AdUserId ADUser + DisplayName string + Phone string + Mobile string + Homeoffice string + Email string + Room string + DepartmentId Department + LocationId Location +} + +type ContactKeywordLink struct { + Contact int + Keyword int +} + +/* ################################################################## */ +/* ENDE DER STRUKTUREN */ +/* ################################################################## */ + func main() { // Signal-Kanal einrichten @@ -51,16 +140,36 @@ func main() { // --- Verzeichnisse konfigurierbar machen ------------------------- staticDir := getenv("BLOG_STATIC_DIR", "./static") templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates") - /*storeEnabled := enabled("STORE_ENABLE", false)*/ - /*TickCatalog = nil - if err := LoadTickCatalog(ticksDir + "/ticks.json"); err != nil { - fmt.Println("Fehler beim Laden:", err) + cfg := Config{ + DSN: "hikos:hikos@tcp(10.10.5.31:3306)/hikos?parseTime=true", + LDAPURL: "ldaps://ldaps.example.com:636", + LDAPBindPattern: "uid=%s,ou=users,dc=example,dc=com", + SessionTTL: 24 * time.Hour, } - fmt.Println("Geladener Katalog:", TickCatalog) + db, err := sql.Open("mysql", cfg.DSN) + if err != nil { + log.Fatal(err) + } + if err := db.Ping(); err != nil { + log.Fatal(err) + } - cloneRepo(gitRepo, gitBranch, gitDir)*/ + store := &SessionStore{DB: db} + auth := &LDAPAuthenticator{ + URL: cfg.LDAPURL, + BindPattern: cfg.LDAPBindPattern, + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + }, + } + + srv := &Server{ + cfg: cfg, + sessions: store, + auth: auth, + } funcs := template.FuncMap{ "now": time.Now, // jetzt‑Zeit bereitstellen @@ -68,28 +177,48 @@ func main() { // Basislayout zuerst parsen layout := template.Must(template.New("base").Funcs(funcs).ParseFiles(templatesDir + "/base.html")) + tplFull := template.Must(layout.Clone()) + template.Must(tplFull.Funcs(funcs).ParseFiles(templatesDir+"/kontaktliste.html", templatesDir+"/schlagwortliste.html")) - // LIST‑Seite: base + list.html - /*tplList = template.Must(layout.Clone()) - template.Must(tplList.Funcs(funcs).ParseFiles(templatesDir + "/list.html")) - tplArticle = template.Must(layout.Clone()) - template.Must(tplArticle.Funcs(funcs).ParseFiles(templatesDir + "/article.html")) - tplPage := template.Must(layout.Clone()) - template.Must(tplPage.ParseFiles(templatesDir + "/page.html"))*/ + tplKontakt := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html")) + tplSchlagwort := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/schlagwortliste.html")) mux := http.NewServeMux() + mux.HandleFunc("/login", srv.loginHandler) + mux.Handle("/protected", srv.withAuth(http.HandlerFunc(srv.protectedHandler))) + // Handler für / mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - layout.ExecuteTemplate(w, "layout", nil) + tplFull.ExecuteTemplate(w, "layout", nil) }) - mux.HandleFunc("/post/", func(w http.ResponseWriter, r *http.Request) { - + mux.HandleFunc("/htmx/kontakt", func(w http.ResponseWriter, r *http.Request) { + tplKontakt.ExecuteTemplate(w, "kontakt", nil) }) - mux.HandleFunc("/page/", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/htmx/kontaktbyschlagwort", func(w http.ResponseWriter, r *http.Request) { + tplKontakt.ExecuteTemplate(w, "kontakt", nil) + }) + mux.HandleFunc("/htmx/amt", func(w http.ResponseWriter, r *http.Request) { + tplKontakt.ExecuteTemplate(w, "kontakt", nil) + }) + + mux.HandleFunc("/htmx/raum", func(w http.ResponseWriter, r *http.Request) { + tplKontakt.ExecuteTemplate(w, "kontakt", nil) + }) + + mux.HandleFunc("/htmx/gebaeude", func(w http.ResponseWriter, r *http.Request) { + tplKontakt.ExecuteTemplate(w, "kontakt", nil) + }) + + mux.HandleFunc("/htmx/schlagwort", func(w http.ResponseWriter, r *http.Request) { + tplSchlagwort.ExecuteTemplate(w, "schlagwort", nil) + }) + + mux.HandleFunc("/htmx/schlagwortbykontakt", func(w http.ResponseWriter, r *http.Request) { + tplSchlagwort.ExecuteTemplate(w, "schlagwort", nil) }) mux.Handle("/static/", cacheControl(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))) @@ -98,7 +227,23 @@ func main() { }) - StopServer(http.ListenAndServe(":8080", mux)) + server := &http.Server{ + /*Addr: ":8443",*/ + Addr: ":8080", + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + } + + log.Println("Listening on https://localhost:8080 …") + /*if err := server.ListenAndServeTLS("cert.pem", "key.pem"); err != nil { + StopServer(err) + }*/ + if err := server.ListenAndServe(); err != nil { + StopServer(err) + } + //StopServer(http.ListenAndServe(":8080", mux)) } @@ -116,3 +261,185 @@ func StopServer(e error) { prepareExit() fmt.Println("~", "Server stopped!") } + +/* ################################################################## */ +/* START LDAP UND GESICHERTE SEITEN */ +/* ################################################################## */ + +// Config bundles all runtime‑adjustable settings. +type Config struct { + DSN string // MySQL DSN → "user:pass@tcp(127.0.0.1:3306)/app?parseTime=true" + LDAPURL string // full URL → "ldaps://ldap.example.com:636" + LDAPBindPattern string // DN pattern → "uid=%s,ou=users,dc=example,dc=com" + SessionTTL time.Duration // session validity +} + +type contextKey string + +const userKey contextKey = "username" + +// ---- SESSION STORE ---- + +type SessionStore struct { + DB *sql.DB +} + +// generateRandomToken returns a URL‑safe base64 random string. +func generateRandomToken(n int) (string, error) { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func (s *SessionStore) Create(username string, ttl time.Duration) (string, error) { + token, err := generateRandomToken(32) + if err != nil { + return "", err + } + expires := time.Now().Add(ttl) + _, err = s.DB.Exec(`INSERT INTO sessions (username, token, expires_at) VALUES (?, ?, ?)`, username, token, expires) + return token, err +} + +func (s *SessionStore) GetUsername(token string) (string, error) { + var username string + var expires time.Time + err := s.DB.QueryRow(`SELECT username, expires_at FROM sessions WHERE token = ?`, token).Scan(&username, &expires) + if err != nil { + return "", err + } + if time.Now().After(expires) { + s.Delete(token) + return "", errors.New("session expired") + } + return username, nil +} + +func (s *SessionStore) Delete(token string) { + s.DB.Exec(`DELETE FROM sessions WHERE token = ?`, token) +} + +// ---- LDAP AUTHENTICATOR ---- + +type LDAPAuthenticator struct { + URL string + BindPattern string + TLSConfig *tls.Config +} + +func (a *LDAPAuthenticator) Authenticate(username, password string) error { + if password == "" { + return errors.New("leer Passwort") + } + conn, err := ldap.DialURL(a.URL, ldap.DialWithTLSConfig(a.TLSConfig)) + + if err != nil { + return fmt.Errorf("ldap dial: %w", err) + } + defer conn.Close() + + dn := fmt.Sprintf(a.BindPattern, ldap.EscapeFilter(username)) + if err := conn.Bind(dn, password); err != nil { + return fmt.Errorf("ldap bind: %w", err) + } + return nil +} + +// ---- HTTP SERVER ---- + +type Server struct { + cfg Config + sessions *SessionStore + auth *LDAPAuthenticator +} + +func (s *Server) loginHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + user := strings.TrimSpace(r.Form.Get("username")) + pass := r.Form.Get("password") + + if err := s.auth.Authenticate(user, pass); err != nil { + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + token, err := s.sessions.Create(user, s.cfg.SessionTTL) + if err != nil { + log.Println("cannot create session:", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + http.SetCookie(w, &http.Cookie{ + Name: "session_token", + Value: token, + Expires: time.Now().Add(s.cfg.SessionTTL), + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + }) + fmt.Fprintln(w, "ok") +} + +func (s *Server) withAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := r.Cookie("session_token") + if err != nil { + http.Error(w, "unauthenticated", http.StatusUnauthorized) + return + } + user, err := s.sessions.GetUsername(c.Value) + if err != nil { + http.Error(w, "unauthenticated", http.StatusUnauthorized) + http.Redirect(w, r, "/login", http.StatusMovedPermanently) + return + } + ctx := context.WithValue(r.Context(), userKey, user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +// authAware builds a handler that can treat auth as optional or mandatory. +// +// required == true → redirect to /login if not authenticated. +// required == false → choose between unauth (A) and auth (B) branch. +func (s *Server) authAware(required bool, unauth, auth http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var username string + if c, err := r.Cookie("session_token"); err == nil { + if u, err := s.sessions.GetUsername(c.Value); err == nil { + username = u + } + } + isAuth := username != "" + if isAuth { + r = r.WithContext(context.WithValue(r.Context(), userKey, username)) + } + + if required { + if !isAuth { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + auth.ServeHTTP(w, r) // C + return + } + + if isAuth { + auth.ServeHTTP(w, r) // B + } else { + unauth.ServeHTTP(w, r) // A + } + }) +} + +func (s *Server) protectedHandler(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(userKey).(string) + fmt.Fprintf(w, "Hallo %s, du bist authentifiziert!", user) +} diff --git a/static/css/main.css b/static/css/main.css index db5bf3a..207f296 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -18,8 +18,8 @@ :root { /* Light theme */ --bg: #f5f7fa; - --bg-alt: #ffffff; - --card-bg: #ffffff; + --bg-alt: #f5f7fa; + --card-bg: #f5f7fa; --text: #000000; --text-muted: #1f2933; --accent: #2563eb; /* Indigo‑600 */ @@ -37,7 +37,7 @@ /* Dark mode (optional) */ @media (prefers-color-scheme: dark) { :root { - --bg: #0d1117; + --bg: #161b22; --bg-alt: #161b22; --card-bg: #161b22; --text: #ffffff; @@ -127,7 +127,7 @@ #bereich-b .top { margin-top: 10px; - height: 35px; + /*height: 35px;*/ display: grid; width: 100%; grid-template-columns: 100%; diff --git a/static/templates/base.html b/static/templates/base.html index 7d0d38c..0d5e59e 100644 --- a/static/templates/base.html +++ b/static/templates/base.html @@ -18,117 +18,13 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTelefonMobilHomeofficeAmtRaumGebäude
Teil 1Teil 2Teil 3Teil 4Teil 5Teil 6Teil 7
Teil 8Teil 9Teil 10Teil 11Teil 12Teil 13Teil 14
-
+ {{ template "kontakt" . }}
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Stichwort
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
Teil 1
Teil 2
-
+ {{ template "schlagwort" . }}
diff --git a/static/templates/kontaktliste.html b/static/templates/kontaktliste.html new file mode 100644 index 0000000..65922b3 --- /dev/null +++ b/static/templates/kontaktliste.html @@ -0,0 +1,39 @@ +{{ define "kontakt" }} +
+ + + + + + + + + + + + + + + + + + + + + + + {{ range .Kontakte }} + + + + + + + + + + {{ end }} + +
NameTelefonMobilHomeofficeAmtRaumGebäude
{{ .Name }}{{ .Telefon }}{{ .Mobil }}{{ .Homeoffice }}{{ .Amt }}{{ .Raum }}{{ .Gebaeude }}
+
+{{ end }} \ No newline at end of file diff --git a/static/templates/schlagwortliste.html b/static/templates/schlagwortliste.html new file mode 100644 index 0000000..fbddc82 --- /dev/null +++ b/static/templates/schlagwortliste.html @@ -0,0 +1,19 @@ +{{ define "schlagwort" }} +
+ + + + + + + + + + + {{ range .Kontakte }} + + {{ end }} + +
Stichwort
{{ .Name }}
+
+{{ end }} \ No newline at end of file