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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- Name |
- Telefon |
- Mobil |
- Homeoffice |
- Amt |
- Raum |
- Gebäude |
-
-
-
-
- Teil 1 |
- Teil 2 |
- Teil 3 |
- Teil 4 |
- Teil 5 |
- Teil 6 |
- Teil 7 |
-
-
- Teil 8 |
- Teil 9 |
- Teil 10 |
- Teil 11 |
- Teil 12 |
- Teil 13 |
- Teil 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" . }}