Updates und Bugfixes
This commit is contained in:
158
main.go
158
main.go
@@ -15,6 +15,7 @@ import (
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -114,6 +115,20 @@ type Contact struct {
|
||||
LocationId sql.NullInt64
|
||||
}
|
||||
|
||||
type SimplifiedContact struct {
|
||||
Id int
|
||||
OwnerId sql.NullInt64
|
||||
AdUserId sql.NullInt64
|
||||
DisplayName string
|
||||
Phone string
|
||||
Mobile string
|
||||
Homeoffice string
|
||||
Email string
|
||||
Room string
|
||||
DepartmentId string
|
||||
LocationId string
|
||||
}
|
||||
|
||||
type ContactKeywordLink struct {
|
||||
Contact sql.NullInt64
|
||||
Keyword sql.NullInt64
|
||||
@@ -131,6 +146,12 @@ type DataPort struct {
|
||||
/*Links []ContactKeywordLink*/
|
||||
}
|
||||
|
||||
type SimplifiedDataPort struct {
|
||||
Contacts []SimplifiedContact
|
||||
Keywords []Keyword
|
||||
/*Links []ContactKeywordLink*/
|
||||
}
|
||||
|
||||
/* ################################################################## */
|
||||
/* ENDE DER STRUKTUREN */
|
||||
/* ################################################################## */
|
||||
@@ -144,8 +165,15 @@ func (s *Server) privateHello(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "Hallo %s - hier deine persönlichen Daten", user)
|
||||
}
|
||||
|
||||
func GetDataReturnDataPort(QueryContact, QueryKeyword string) DataPort {
|
||||
var contList []Contact
|
||||
func NullInt64ToString(n sql.NullInt64) string {
|
||||
if !n.Valid {
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(n.Int64, 10)
|
||||
}
|
||||
|
||||
func GetDataReturnDataPort(QueryContact, QueryKeyword string) SimplifiedDataPort {
|
||||
var contList []SimplifiedContact
|
||||
var keywordList []Keyword
|
||||
|
||||
if QueryContact != "" {
|
||||
@@ -154,7 +182,7 @@ func GetDataReturnDataPort(QueryContact, QueryKeyword string) DataPort {
|
||||
fmt.Println("a", err)
|
||||
}
|
||||
for rowsContact.Next() {
|
||||
var c Contact
|
||||
var c SimplifiedContact
|
||||
err = rowsContact.Scan(&c.Id, &c.OwnerId, &c.AdUserId, &c.DisplayName, &c.Phone, &c.Mobile, &c.Homeoffice, &c.Email, &c.Room, &c.DepartmentId, &c.LocationId)
|
||||
if err != nil {
|
||||
fmt.Println("b", err)
|
||||
@@ -178,11 +206,15 @@ func GetDataReturnDataPort(QueryContact, QueryKeyword string) DataPort {
|
||||
}
|
||||
}
|
||||
|
||||
return DataPort{Contacts: contList, Keywords: keywordList}
|
||||
return SimplifiedDataPort{Contacts: contList, Keywords: keywordList}
|
||||
}
|
||||
|
||||
func makeContactTableJoin() string {
|
||||
return "SELECT c.contact_id, c.contact_owner_id, c.contact_aduser_id, c.contact_displayname, c.contact_phone, c.contact_mobile, c.contact_homeoffice, c.contact_email, c.contact_room, d.department_name, l.location_name FROM contact c JOIN department d ON (c.contact_department_id = d.department_id) JOIN location l ON (c.contact_location_id = l.location_id) "
|
||||
}
|
||||
|
||||
func (s *Server) ListPublic(w http.ResponseWriter, r *http.Request) {
|
||||
D := GetDataReturnDataPort("SELECT * FROM contact c WHERE c.contact_owner_id = -1;", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;")
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1;", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("base").Funcs(funcs).ParseFiles(templatesDir + "/base.html"))
|
||||
@@ -194,19 +226,31 @@ func (s *Server) ListPublic(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) ListPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
D := GetDataReturnDataPort("SELECT * FROM contact c WHERE c.contact_owner_id = -1 OR c.contact_owner_id = 1;", "SELECT * FROM keyword c WHERE c.keyword_owner = -1 OR c.keyword_owner = 1;")
|
||||
user := r.Context().Value(userKey).(string)
|
||||
/*fmt.Fprintf(w, "Hallo %s - hier deine persönlichen Daten", user)*/
|
||||
fmt.Println("Hallo "+user+" hier deine persönlichen Daten", user)
|
||||
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 OR c.contact_owner_id = 1;", "SELECT * FROM keyword c WHERE c.keyword_owner = -1 OR c.keyword_owner = 1;")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
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"))
|
||||
tplFull.ExecuteTemplate(w, "layout", D)
|
||||
/*user := r.Context().Value(userKey).(string)
|
||||
fmt.Fprintf(w, "Hallo %s - hier deine persönlichen Daten", user)*/
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) XcontactPublic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("search"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (c.contact_displayname LIKE '%"+sparam+"%' OR c.contact_phone LIKE '%"+sparam+"%' OR c.contact_mobile LIKE '%"+sparam+"%' OR c.contact_homeoffice LIKE '%"+sparam+"%');", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html"))
|
||||
layout.ExecuteTemplate(w, "kontakt", D)
|
||||
}
|
||||
|
||||
func (s *Server) XcontactPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -214,7 +258,16 @@ func (s *Server) XcontactPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) XdepartmentPublic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("search"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (d.department_name LIKE '%"+sparam+"%');", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html"))
|
||||
layout.ExecuteTemplate(w, "kontakt", D)
|
||||
}
|
||||
|
||||
func (s *Server) XdepartmentPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -222,7 +275,16 @@ func (s *Server) XdepartmentPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) XroomPublic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("search"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (c.contact_room LIKE '%"+sparam+"%');", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html"))
|
||||
layout.ExecuteTemplate(w, "kontakt", D)
|
||||
}
|
||||
|
||||
func (s *Server) XroomPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -230,7 +292,16 @@ func (s *Server) XroomPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) XlocationPublic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("search"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (l.location_name LIKE '%"+sparam+"%');", "SELECT * FROM keyword c WHERE c.keyword_owner = -1;")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html"))
|
||||
layout.ExecuteTemplate(w, "kontakt", D)
|
||||
}
|
||||
|
||||
func (s *Server) XlocationPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -238,15 +309,42 @@ func (s *Server) XlocationPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) XkeywordPublic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("search"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (l.location_name LIKE '%"+sparam+"%');", "SELECT * FROM keyword c WHERE c.keyword_owner = -1 AND c.keyword_name LIKE '%"+sparam+"%';")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("schlagwort").Funcs(funcs).ParseFiles(templatesDir + "/schlagwortliste.html"))
|
||||
layout.ExecuteTemplate(w, "schlagwort", D)
|
||||
}
|
||||
|
||||
func (s *Server) XkeywordPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("search"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (l.location_name LIKE '%"+sparam+"%');", "SELECT * FROM keyword c WHERE (c.keyword_owner = -1 OR c.keyword_owner = 1) AND c.keyword_name LIKE '%"+sparam+"%';")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("schlagwort").Funcs(funcs).ParseFiles(templatesDir + "/schlagwortliste.html"))
|
||||
layout.ExecuteTemplate(w, "schlagwort", D)
|
||||
}
|
||||
|
||||
func (s *Server) XkwbctPublic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sparam := strings.TrimSpace(r.Form.Get("cid"))
|
||||
D := GetDataReturnDataPort(makeContactTableJoin()+"WHERE c.contact_owner_id = -1 AND (l.location_name LIKE '%"+sparam+"%');", "SELECT c.* FROM keyword c JOIN contactkeyword z ON (z.contactkeyword_keyword = c.keyword_id) WHERE z.contactkeyword_contact = "+sparam+";")
|
||||
funcs := template.FuncMap{"now": time.Now}
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
layout := template.Must(template.New("schlagwort").Funcs(funcs).ParseFiles(templatesDir + "/schlagwortliste.html"))
|
||||
layout.ExecuteTemplate(w, "schlagwort", D)
|
||||
}
|
||||
|
||||
func (s *Server) XkwbctPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -264,6 +362,22 @@ func (s *Server) XctbkwPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
var CFG Config
|
||||
var DB *sql.DB
|
||||
|
||||
type Ranger struct {
|
||||
mu sync.RWMutex
|
||||
xContacts map[string]struct{}
|
||||
}
|
||||
|
||||
/*func newRanger() *Ranger {
|
||||
return &Ranger{
|
||||
blocks: make(map[string]map[netip.Prefix]struct{}),
|
||||
whites: make(map[netip.Addr]struct{}),
|
||||
}
|
||||
}*/
|
||||
|
||||
type XServer struct {
|
||||
ranger *Ranger
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// Signal-Kanal einrichten
|
||||
@@ -283,7 +397,7 @@ func main() {
|
||||
templatesDir := getenv("BLOG_TEMPLATES_DIR", "./static/templates")
|
||||
|
||||
CFG = Config{
|
||||
DSN: "hikos:hikos@tcp(10.10.5.31:3306)/hikos?parseTime=true",
|
||||
DSN: "hikos:hikos@tcp(db-mysql-ubnt-a.stadt-hilden.de:3306)/hikos?parseTime=true",
|
||||
LDAPURL: "ldaps://ldaps.example.com:636",
|
||||
LDAPBindPattern: "uid=%s,ou=users,dc=example,dc=com",
|
||||
SessionTTL: 24 * time.Hour,
|
||||
@@ -322,8 +436,8 @@ func main() {
|
||||
tplFull := template.Must(layout.Clone())
|
||||
template.Must(tplFull.Funcs(funcs).ParseFiles(templatesDir+"/kontaktliste.html", templatesDir+"/schlagwortliste.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"))
|
||||
/*tplKontakt := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/kontaktliste.html"))
|
||||
tplSchlagwort := template.Must(template.New("kontakt").Funcs(funcs).ParseFiles(templatesDir + "/schlagwortliste.html"))*/
|
||||
|
||||
layoutSSO := template.Must(template.New("sso").Funcs(funcs).ParseFiles(templatesDir + "/login.html"))
|
||||
|
||||
@@ -349,7 +463,7 @@ func main() {
|
||||
mux.Handle("/htmx/keywordbycontact", srv.authAware(false, http.HandlerFunc(srv.XkwbctPublic), http.HandlerFunc(srv.XkwbctPrivate)))
|
||||
mux.Handle("/htmx/contactbykeyword", srv.authAware(false, http.HandlerFunc(srv.XctbkwPublic), http.HandlerFunc(srv.XctbkwPrivate)))
|
||||
|
||||
mux.HandleFunc("/htmx/contact", func(w http.ResponseWriter, r *http.Request) {
|
||||
/*mux.HandleFunc("/htmx/contact", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
return
|
||||
@@ -391,7 +505,7 @@ func main() {
|
||||
|
||||
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)))))
|
||||
|
||||
@@ -555,7 +669,7 @@ func (s *Server) loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
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})
|
||||
http.SetCookie(w, &http.Cookie{Name: "session_token", Value: token, Expires: time.Now().Add(s.cfg.SessionTTL), Path: "/", Secure: false, HttpOnly: true, SameSite: http.SameSiteStrictMode})
|
||||
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
@@ -596,7 +710,7 @@ func (s *Server) authAware(required bool, unauth, auth http.Handler) http.Handle
|
||||
|
||||
if required {
|
||||
if !isAuth {
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
http.Redirect(w, r, "/sso", http.StatusFound)
|
||||
return
|
||||
}
|
||||
auth.ServeHTTP(w, r) // C
|
||||
|
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
/* Dark mode (optional) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/*@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #161b22;
|
||||
--bg-alt: #161b22;
|
||||
@@ -48,7 +48,7 @@
|
||||
--code-border: #30363d;
|
||||
--shadow: 0 4px 16px rgba(0,0,0,.32);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -175,7 +175,7 @@
|
||||
|
||||
th, td {
|
||||
border: 1px solid #ddd; /* Beispiel für einen Rahmen */
|
||||
padding: 8px;
|
||||
padding: 4px;
|
||||
text-align: center; /* Beispiel für Textzentrierung */
|
||||
}
|
||||
|
||||
|
@@ -14,16 +14,16 @@
|
||||
<div class="container">
|
||||
<div id="bereich-a">
|
||||
<div class="top">
|
||||
<input type="text" name="search" placeholder="Suchfeld" autocomplete="false" hx-post="/htmx/contact" hx-trigger="keyup changed delay:500ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Amt" autocomplete="false" hx-post="/htmx/department" hx-trigger="keyup changed delay:500ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Raum" autocomplete="false" hx-post="/htmx/room" hx-trigger="keyup changed delay:500ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Gebäude" autocomplete="false" hx-post="/htmx/location" hx-trigger="keyup changed delay:500ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Suchfeld" autocomplete="false" hx-post="/htmx/contact" hx-trigger="keyup changed delay:200ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Amt" autocomplete="false" hx-post="/htmx/department" hx-trigger="keyup changed delay:200ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Raum" autocomplete="false" hx-post="/htmx/room" hx-trigger="keyup changed delay:200ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
<input type="text" name="search" placeholder="Gebäude" autocomplete="false" hx-post="/htmx/location" hx-trigger="keyup changed delay:200ms" hx-target="#z-1" hx-swap="outerHTML" />
|
||||
</div>
|
||||
{{ template "kontakt" . }}
|
||||
</div>
|
||||
<div id="bereich-b">
|
||||
<div class="top">
|
||||
<input type="text" placeholder="Stichwort" />
|
||||
<input type="text" name="search" placeholder="Stichwort" hx-post="/htmx/keyword" hx-trigger="keyup changed delay:200ms" hx-target="#z-2" hx-swap="outerHTML" />
|
||||
</div>
|
||||
{{ template "schlagwort" . }}
|
||||
</div>
|
||||
|
@@ -23,14 +23,14 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Contacts }}
|
||||
<tr>
|
||||
<tr hx-post="/htmx/keywordbycontact" hx-trigger="click delay:200ms" hx-target="#z-2" hx-swap="outerHTML" hx-vals='{"cid": "{{ .Id }}"}'>
|
||||
<td>{{ if .DisplayName }}{{ .DisplayName }}{{ end }}</td>
|
||||
<td>{{ if .Phone }}{{ .Phone }}{{ end }}</td>
|
||||
<td>{{ if .Mobile }}{{ .Mobile }}{{ end }}</td>
|
||||
<td>{{ if .Homeoffice }}{{ .Homeoffice }}{{ end }}</td>
|
||||
<td>{{ if .DepartmentId.Valid }}{{ .DepartmentId.Int64 }}{{ end }}</td>
|
||||
<td>{{ if .DepartmentId }}{{ .DepartmentId }}{{ end }}</td>
|
||||
<td>{{ if .Room }}{{ .Room }}{{ end }}</td>
|
||||
<td>{{ if .LocationId.Valid }}{{ .LocationId.Int64 }}{{ end }}</td>
|
||||
<td>{{ if .LocationId }}{{ .LocationId }}{{ end }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
|
Reference in New Issue
Block a user