This commit is contained in:
2025-05-20 11:38:54 +02:00
parent 52b1da2fe6
commit 95a693bd3b
7 changed files with 434 additions and 128 deletions

11
go.mod
View File

@@ -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
)

14
go.sum Normal file
View File

@@ -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=

363
main.go
View File

@@ -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, // jetztZeit 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"))
// LISTSeite: 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 runtimeadjustable 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 URLsafe 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)
}

View File

@@ -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; /* Indigo600 */
@@ -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%;

View File

@@ -18,117 +18,13 @@
<input type="text" placeholder="Raum" />
<input type="text" placeholder="Gebäude" />
</div>
<div class="bottom">
<table>
<colgroup>
<col style="width: 27.5%">
<col style="width: 7.5%">
<col style="width: 7.5%">
<col style="width: 7.5%">
<col style="width: 5%">
<col style="width: 7.5%">
<col style="width: 10%">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Telefon</th>
<th>Mobil</th>
<th>Homeoffice</th>
<th>Amt</th>
<th>Raum</th>
<th>Gebäude</th>
</tr>
</thead>
<tbody>
<tr>
<td>Teil 1</td>
<td>Teil 2</td>
<td>Teil 3</td>
<td>Teil 4</td>
<td>Teil 5</td>
<td>Teil 6</td>
<td>Teil 7</td>
</tr>
<tr>
<td>Teil 8</td>
<td>Teil 9</td>
<td>Teil 10</td>
<td>Teil 11</td>
<td>Teil 12</td>
<td>Teil 13</td>
<td>Teil 14</td>
</tr>
</tbody>
</table>
</div>
{{ template "kontakt" . }}
</div>
<div id="bereich-b">
<div class="top">
<input type="text" placeholder="Stichwort" />
</div>
<div class="bottom">
<table>
<colgroup>
<col style="width: 100%">
</colgroup>
<thead>
<tr>
<th>Stichwort</th>
</tr>
</thead>
<tbody>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
<tr><td>Teil 1</td></tr>
<tr><td>Teil 2</td></tr>
</tbody>
</table>
</div>
{{ template "schlagwort" . }}
</div>
</div>
</body>

View File

@@ -0,0 +1,39 @@
{{ define "kontakt" }}
<div class="bottom" id="z-1">
<table>
<colgroup>
<col style="width: 27.5%">
<col style="width: 7.5%">
<col style="width: 7.5%">
<col style="width: 7.5%">
<col style="width: 5%">
<col style="width: 7.5%">
<col style="width: 10%">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Telefon</th>
<th>Mobil</th>
<th>Homeoffice</th>
<th>Amt</th>
<th>Raum</th>
<th>Gebäude</th>
</tr>
</thead>
<tbody>
{{ range .Kontakte }}
<tr>
<td>{{ .Name }}</td>
<td>{{ .Telefon }}</td>
<td>{{ .Mobil }}</td>
<td>{{ .Homeoffice }}</td>
<td>{{ .Amt }}</td>
<td>{{ .Raum }}</td>
<td>{{ .Gebaeude }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}

View File

@@ -0,0 +1,19 @@
{{ define "schlagwort" }}
<div class="bottom" id="z-2">
<table>
<colgroup>
<col style="width: 100%">
</colgroup>
<thead>
<tr>
<th>Stichwort</th>
</tr>
</thead>
<tbody>
{{ range .Kontakte }}
<tr><td>{{ .Name }}</td></tr>
{{ end }}
</tbody>
</table>
</div>
{{ end }}