mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-08 10:51:09 +00:00
Working user panel
See https://mycorrhiza.wiki/hypha/idea/user_panel It's not pretty, but it works. The next step is to make it look good.
This commit is contained in:
parent
c7e4281398
commit
11e98b2368
@ -124,6 +124,10 @@ func readTokensToUsers() {
|
|||||||
log.Println("Found", len(tmp), "active sessions")
|
log.Println("Found", len(tmp), "active sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SaveUserDatabase() error {
|
||||||
|
return dumpRegistrationCredentials()
|
||||||
|
}
|
||||||
|
|
||||||
func dumpRegistrationCredentials() error {
|
func dumpRegistrationCredentials() error {
|
||||||
tmp := []*User{}
|
tmp := []*User{}
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ func FromRequest(rq *http.Request) *User {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return EmptyUser()
|
return EmptyUser()
|
||||||
}
|
}
|
||||||
return userByToken(cookie.Value)
|
return UserByToken(cookie.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogoutFromRequest logs the user in `rq` out and rewrites the cookie in `w`.
|
// LogoutFromRequest logs the user in `rq` out and rewrites the cookie in `w`.
|
||||||
|
17
user/user.go
17
user/user.go
@ -51,6 +51,14 @@ var minimalRights = map[string]int{
|
|||||||
"admin/shutdown": 4,
|
"admin/shutdown": 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var groups = []string{
|
||||||
|
"anon",
|
||||||
|
"editor",
|
||||||
|
"trusted",
|
||||||
|
"moderator",
|
||||||
|
"admin",
|
||||||
|
}
|
||||||
|
|
||||||
// Group — Right
|
// Group — Right
|
||||||
var groupRight = map[string]int{
|
var groupRight = map[string]int{
|
||||||
"anon": 0,
|
"anon": 0,
|
||||||
@ -60,6 +68,15 @@ var groupRight = map[string]int{
|
|||||||
"admin": 4,
|
"admin": 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidGroup(group string) bool {
|
||||||
|
for _, grp := range groups {
|
||||||
|
if grp == group {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func EmptyUser() *User {
|
func EmptyUser() *User {
|
||||||
return &User{
|
return &User{
|
||||||
Name: "anon",
|
Name: "anon",
|
||||||
|
@ -56,18 +56,18 @@ func HasUsername(username string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CredentialsOK(username, password string) bool {
|
func CredentialsOK(username, password string) bool {
|
||||||
return userByName(username).isCorrectPassword(password)
|
return UserByName(username).isCorrectPassword(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userByToken(token string) *User {
|
func UserByToken(token string) *User {
|
||||||
if usernameUntyped, ok := tokens.Load(token); ok {
|
if usernameUntyped, ok := tokens.Load(token); ok {
|
||||||
username := usernameUntyped.(string)
|
username := usernameUntyped.(string)
|
||||||
return userByName(username)
|
return UserByName(username)
|
||||||
}
|
}
|
||||||
return EmptyUser()
|
return EmptyUser()
|
||||||
}
|
}
|
||||||
|
|
||||||
func userByName(username string) *User {
|
func UserByName(username string) *User {
|
||||||
if userUntyped, ok := users.Load(username); ok {
|
if userUntyped, ok := users.Load(username); ok {
|
||||||
user := userUntyped.(*User)
|
user := userUntyped.(*User)
|
||||||
return user
|
return user
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>Manage users</h1>
|
<h1>Manage users</h1>
|
||||||
|
|
||||||
<form action="/admin/reindex-users" method="get">
|
<form action="/admin/reindex-users" method="post">
|
||||||
<button class="btn btn_accent" type="submit">Reindex users</button>
|
<button class="btn btn_accent" type="submit">Reindex users</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -17,14 +17,18 @@
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Group</th>
|
<th>Group</th>
|
||||||
<th>Registered at</th>
|
<th>Registered at</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for _, u := range userList %}
|
{% for _, u := range userList %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{%s= u.Name %}</td>
|
<td>{%s u.Name %}</td>
|
||||||
<td>{%s= u.Group %}</td>
|
<td>{%s u.Group %}</td>
|
||||||
<td>{%s= u.RegisteredAt.Format("2006-01-02 15:04:05-0700") %}</td>
|
<td>{%s u.RegisteredAt.Format("2006-01-02 15:04:05-0700") %}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/users/{%u u.Name %}/edit">Edit</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -32,3 +36,26 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
{% endfunc %}
|
{% endfunc %}
|
||||||
|
|
||||||
|
{% func AdminUsersUserHTML(u *user.User) %}
|
||||||
|
<div class="layout">
|
||||||
|
<main class="main-width">
|
||||||
|
<h1>{%s u.Name %}</h1>
|
||||||
|
|
||||||
|
<form action="" method="post">
|
||||||
|
<label for="group">Group:</label>
|
||||||
|
<select id="group" name="group">
|
||||||
|
<option{% if u.Group == "anon" %} selected{% endif %}>anon</option>
|
||||||
|
<option{% if u.Group == "editor" %} selected{% endif %}>editor</option>
|
||||||
|
<option{% if u.Group == "trusted" %} selected{% endif %}>trusted</option>
|
||||||
|
<option{% if u.Group == "moderator" %} selected{% endif %}>moderator</option>
|
||||||
|
<option{% if u.Group == "admin" %} selected{% endif %}>admin</option>
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<button class="btn btn_accent" type="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
{% endfunc %}
|
||||||
|
@ -28,7 +28,7 @@ func StreamAdminUsersPanelHTML(qw422016 *qt422016.Writer, userList []*user.User)
|
|||||||
<main class="main-width">
|
<main class="main-width">
|
||||||
<h1>Manage users</h1>
|
<h1>Manage users</h1>
|
||||||
|
|
||||||
<form action="/admin/reindex-users" method="get">
|
<form action="/admin/reindex-users" method="post">
|
||||||
<button class="btn btn_accent" type="submit">Reindex users</button>
|
<button class="btn btn_accent" type="submit">Reindex users</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -40,66 +40,172 @@ func StreamAdminUsersPanelHTML(qw422016 *qt422016.Writer, userList []*user.User)
|
|||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Group</th>
|
<th>Group</th>
|
||||||
<th>Registered at</th>
|
<th>Registered at</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
`)
|
`)
|
||||||
//line views/admin.qtpl:23
|
//line views/admin.qtpl:24
|
||||||
for _, u := range userList {
|
for _, u := range userList {
|
||||||
//line views/admin.qtpl:23
|
//line views/admin.qtpl:24
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
<tr>
|
<tr>
|
||||||
<td>`)
|
<td>`)
|
||||||
//line views/admin.qtpl:25
|
|
||||||
qw422016.N().S(u.Name)
|
|
||||||
//line views/admin.qtpl:25
|
|
||||||
qw422016.N().S(`</td>
|
|
||||||
<td>`)
|
|
||||||
//line views/admin.qtpl:26
|
//line views/admin.qtpl:26
|
||||||
qw422016.N().S(u.Group)
|
qw422016.E().S(u.Name)
|
||||||
//line views/admin.qtpl:26
|
//line views/admin.qtpl:26
|
||||||
qw422016.N().S(`</td>
|
qw422016.N().S(`</td>
|
||||||
<td>`)
|
<td>`)
|
||||||
//line views/admin.qtpl:27
|
//line views/admin.qtpl:27
|
||||||
qw422016.N().S(u.RegisteredAt.Format("2006-01-02 15:04:05-0700"))
|
qw422016.E().S(u.Group)
|
||||||
//line views/admin.qtpl:27
|
//line views/admin.qtpl:27
|
||||||
qw422016.N().S(`</td>
|
qw422016.N().S(`</td>
|
||||||
|
<td>`)
|
||||||
|
//line views/admin.qtpl:28
|
||||||
|
qw422016.E().S(u.RegisteredAt.Format("2006-01-02 15:04:05-0700"))
|
||||||
|
//line views/admin.qtpl:28
|
||||||
|
qw422016.N().S(`</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/users/`)
|
||||||
|
//line views/admin.qtpl:30
|
||||||
|
qw422016.N().U(u.Name)
|
||||||
|
//line views/admin.qtpl:30
|
||||||
|
qw422016.N().S(`/edit">Edit</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`)
|
`)
|
||||||
//line views/admin.qtpl:29
|
//line views/admin.qtpl:33
|
||||||
}
|
}
|
||||||
//line views/admin.qtpl:29
|
//line views/admin.qtpl:33
|
||||||
qw422016.N().S(`
|
qw422016.N().S(`
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
`)
|
`)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
func WriteAdminUsersPanelHTML(qq422016 qtio422016.Writer, userList []*user.User) {
|
func WriteAdminUsersPanelHTML(qq422016 qtio422016.Writer, userList []*user.User) {
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
StreamAdminUsersPanelHTML(qw422016, userList)
|
StreamAdminUsersPanelHTML(qw422016, userList)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
qt422016.ReleaseWriter(qw422016)
|
qt422016.ReleaseWriter(qw422016)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
}
|
}
|
||||||
|
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
func AdminUsersPanelHTML(userList []*user.User) string {
|
func AdminUsersPanelHTML(userList []*user.User) string {
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
qb422016 := qt422016.AcquireByteBuffer()
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
WriteAdminUsersPanelHTML(qb422016, userList)
|
WriteAdminUsersPanelHTML(qb422016, userList)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
qs422016 := string(qb422016.B)
|
qs422016 := string(qb422016.B)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
qt422016.ReleaseByteBuffer(qb422016)
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
return qs422016
|
return qs422016
|
||||||
//line views/admin.qtpl:34
|
//line views/admin.qtpl:38
|
||||||
|
}
|
||||||
|
|
||||||
|
//line views/admin.qtpl:40
|
||||||
|
func StreamAdminUsersUserHTML(qw422016 *qt422016.Writer, u *user.User) {
|
||||||
|
//line views/admin.qtpl:40
|
||||||
|
qw422016.N().S(`
|
||||||
|
<div class="layout">
|
||||||
|
<main class="main-width">
|
||||||
|
<h1>`)
|
||||||
|
//line views/admin.qtpl:43
|
||||||
|
qw422016.E().S(u.Name)
|
||||||
|
//line views/admin.qtpl:43
|
||||||
|
qw422016.N().S(`</h1>
|
||||||
|
|
||||||
|
<form action="" method="post">
|
||||||
|
<label for="group">Group:</label>
|
||||||
|
<select id="group" name="group">
|
||||||
|
<option`)
|
||||||
|
//line views/admin.qtpl:48
|
||||||
|
if u.Group == "anon" {
|
||||||
|
//line views/admin.qtpl:48
|
||||||
|
qw422016.N().S(` selected`)
|
||||||
|
//line views/admin.qtpl:48
|
||||||
|
}
|
||||||
|
//line views/admin.qtpl:48
|
||||||
|
qw422016.N().S(`>anon</option>
|
||||||
|
<option`)
|
||||||
|
//line views/admin.qtpl:49
|
||||||
|
if u.Group == "editor" {
|
||||||
|
//line views/admin.qtpl:49
|
||||||
|
qw422016.N().S(` selected`)
|
||||||
|
//line views/admin.qtpl:49
|
||||||
|
}
|
||||||
|
//line views/admin.qtpl:49
|
||||||
|
qw422016.N().S(`>editor</option>
|
||||||
|
<option`)
|
||||||
|
//line views/admin.qtpl:50
|
||||||
|
if u.Group == "trusted" {
|
||||||
|
//line views/admin.qtpl:50
|
||||||
|
qw422016.N().S(` selected`)
|
||||||
|
//line views/admin.qtpl:50
|
||||||
|
}
|
||||||
|
//line views/admin.qtpl:50
|
||||||
|
qw422016.N().S(`>trusted</option>
|
||||||
|
<option`)
|
||||||
|
//line views/admin.qtpl:51
|
||||||
|
if u.Group == "moderator" {
|
||||||
|
//line views/admin.qtpl:51
|
||||||
|
qw422016.N().S(` selected`)
|
||||||
|
//line views/admin.qtpl:51
|
||||||
|
}
|
||||||
|
//line views/admin.qtpl:51
|
||||||
|
qw422016.N().S(`>moderator</option>
|
||||||
|
<option`)
|
||||||
|
//line views/admin.qtpl:52
|
||||||
|
if u.Group == "admin" {
|
||||||
|
//line views/admin.qtpl:52
|
||||||
|
qw422016.N().S(` selected`)
|
||||||
|
//line views/admin.qtpl:52
|
||||||
|
}
|
||||||
|
//line views/admin.qtpl:52
|
||||||
|
qw422016.N().S(`>admin</option>
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<button class="btn btn_accent" type="submit">Update</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
}
|
||||||
|
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
func WriteAdminUsersUserHTML(qq422016 qtio422016.Writer, u *user.User) {
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
StreamAdminUsersUserHTML(qw422016, u)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
qt422016.ReleaseWriter(qw422016)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
}
|
||||||
|
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
func AdminUsersUserHTML(u *user.User) string {
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
qb422016 := qt422016.AcquireByteBuffer()
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
WriteAdminUsersUserHTML(qb422016, u)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
qs422016 := string(qb422016.B)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
qt422016.ReleaseByteBuffer(qb422016)
|
||||||
|
//line views/admin.qtpl:61
|
||||||
|
return qs422016
|
||||||
|
//line views/admin.qtpl:61
|
||||||
}
|
}
|
||||||
|
83
web/admin.go
83
web/admin.go
@ -1,10 +1,13 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"mime"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bouncepaw/mycorrhiza/cfg"
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
||||||
"github.com/bouncepaw/mycorrhiza/user"
|
"github.com/bouncepaw/mycorrhiza/user"
|
||||||
@ -19,7 +22,7 @@ func initAdmin() {
|
|||||||
http.HandleFunc("/admin/shutdown", handlerAdminShutdown)
|
http.HandleFunc("/admin/shutdown", handlerAdminShutdown)
|
||||||
http.HandleFunc("/admin/reindex-users", handlerAdminReindexUsers)
|
http.HandleFunc("/admin/reindex-users", handlerAdminReindexUsers)
|
||||||
|
|
||||||
http.HandleFunc("/admin/users", handlerAdminUsers)
|
http.HandleFunc("/admin/users/", handlerAdminUsers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,31 +52,79 @@ func handlerAdminReindexUsers(w http.ResponseWriter, rq *http.Request) {
|
|||||||
util.PrepareRq(rq)
|
util.PrepareRq(rq)
|
||||||
if user.CanProceed(rq, "admin") && rq.Method == "POST" {
|
if user.CanProceed(rq, "admin") && rq.Method == "POST" {
|
||||||
user.ReadUsersFromFilesystem()
|
user.ReadUsersFromFilesystem()
|
||||||
http.Redirect(w, rq, "/hypha/"+cfg.UserHypha, http.StatusSeeOther)
|
redirectTo := rq.Referer()
|
||||||
|
if redirectTo == "" {
|
||||||
|
redirectTo = "/hypha/"+cfg.UserHypha
|
||||||
|
}
|
||||||
|
http.Redirect(w, rq, redirectTo, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerAdminUsers(w http.ResponseWriter, r *http.Request) {
|
func handlerAdminUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
util.PrepareRq(r)
|
util.PrepareRq(r)
|
||||||
if user.CanProceed(r, "admin") {
|
if user.CanProceed(r, "admin") {
|
||||||
// Get a sorted list of users
|
path := strings.TrimPrefix(r.URL.Path, "/admin/users")
|
||||||
var userList []*user.User
|
parts := strings.Split(path, "/")[1:]
|
||||||
for u := range user.YieldUsers() {
|
|
||||||
userList = append(userList, u)
|
// Users dashboard
|
||||||
|
if len(parts) == 0 {
|
||||||
|
// Get a sorted list of users
|
||||||
|
var userList []*user.User
|
||||||
|
for u := range user.YieldUsers() {
|
||||||
|
userList = append(userList, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(userList, func(i, j int) bool {
|
||||||
|
less := userList[i].RegisteredAt.Before(userList[j].RegisteredAt)
|
||||||
|
return less
|
||||||
|
})
|
||||||
|
|
||||||
|
html := views.AdminUsersPanelHTML(userList)
|
||||||
|
html = views.BaseHTML("Manage users", html, user.FromRequest(r))
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||||||
|
if _, err := io.WriteString(w, html); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(userList, func(i, j int) bool {
|
// User edit page
|
||||||
less := userList[i].RegisteredAt.Before(userList[j].RegisteredAt)
|
if len(parts) == 2 && parts[1] == "edit" {
|
||||||
return less
|
u := user.UserByName(parts[0])
|
||||||
})
|
|
||||||
|
|
||||||
html := views.AdminUsersPanelHTML(userList)
|
if u != nil && u.Name != "anon" {
|
||||||
html = views.BaseHTML("Manage users", html, user.FromRequest(r))
|
if r.Method == http.MethodGet {
|
||||||
|
html := views.AdminUsersUserHTML(u)
|
||||||
|
html = views.BaseHTML(fmt.Sprintf("User %s", u.Name), html, user.FromRequest(r))
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", mime.TypeByExtension(".html"))
|
||||||
_, err := io.WriteString(w, html)
|
if _, err := io.WriteString(w, html); err != nil {
|
||||||
if err != nil {
|
log.Println(err)
|
||||||
log.Println(err)
|
}
|
||||||
|
return
|
||||||
|
} else if r.Method == http.MethodPost {
|
||||||
|
oldGroup := u.Group
|
||||||
|
newGroup := r.PostFormValue("group")
|
||||||
|
if user.ValidGroup(newGroup) {
|
||||||
|
u.Group = newGroup
|
||||||
|
if err := user.SaveUserDatabase(); err != nil {
|
||||||
|
u.Group = oldGroup
|
||||||
|
log.Println(err)
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
io.WriteString(w, err.Error())
|
||||||
|
} else {
|
||||||
|
http.Redirect(w, r, "/admin/users/", http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
io.WriteString(w, "invalid group")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
util.HTTP404Page(w, "404 page not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user