️ Implemented login management

This commit is contained in:
Andreas Schneider 2020-10-28 19:59:29 +01:00
parent 99bce4d1fc
commit 6993a9f58b
5 changed files with 421 additions and 183 deletions

View File

@ -1,92 +1,94 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ShareDAV</title> <title>ShareDAV</title>
<style> <style>
thead { thead {
font-weight: bold; font-weight: bold;
} }
input[type="submit"], input[type="submit"],
input[type="button"] { input[type="button"] {
padding: .5em; padding: .5em;
border-radius: .5em; border-radius: .5em;
border: 0; border: 0;
background-color: #c4e1ff; background-color: #c4e1ff;
font-weight: bold; font-weight: bold;
} }
input[type="submit"].delete { input[type="submit"].delete {
background-color: #e56f83; background-color: #e56f83;
} }
input[type="submit"] { input[type="submit"] {
background-color: #a9ca97; background-color: #a9ca97;
} }
a:before { a:before {
content: " "; content: " ";
color: gray; color: gray;
} }
a, a:visited, a:hover, a:active, a:focus { a, a:visited, a:hover, a:active, a:focus {
/* font-style: normal; */ /* font-style: normal; */
text-decoration: none; text-decoration: none;
color: black; color: black;
} }
#content { #content {
float: right; float: right;
width: calc(100% - 20px); width: calc(100% - 20px);
/* background-color: #F0F0F0; */ /* background-color: #F0F0F0; */
/* border-radius: 1em; */ /* border-radius: 1em; */
/* padding: 10px; */ /* padding: 10px; */
/* height: calc(100vh - 40px);*/ /* height: calc(100vh - 40px);*/
} }
#menu { #menu {
float: left; float: left;
width: 150px; width: 150px;
margin-left: -185px; margin-left: -185px;
background-color: #CCCCCC; background-color: #CCCCCC;
border-radius: .5em; border-radius: .5em;
padding: 10px; padding: 10px;
height: calc(100vh - 40px); height: calc(100vh - 40px);
} }
#menu span { #menu span {
display: block; display: block;
margin-bottom: .5em; margin-bottom: .5em;
} }
#menu hr { #menu hr {
border: 1px solid gray; border: 1px solid gray;
} }
body { body {
font-family: sans-serif; font-family: sans-serif;
margin-left: 200px margin-left: 200px
} }
</style> </style>
{{ block "page-styles" . }}{{ end }} {{ block "page-styles" . }}{{ end }}
</head> </head>
<body> <body>
<div id="content"> <div id="content">
{{ block "page-content" . }}{{ end }} {{ block "page-content" . }}{{ end }}
</div> </div>
<div id="menu"> <div id="menu">
<span style="font-weight: bold;"><a href="./">ShareDAV</a></span> <span style="font-weight: bold;"><a href="./">ShareDAV</a></span>
{{ if .SessionUser }} {{ if .SessionUser }}
{{ if eq .SessionUser.Role "admin" }} {{ if eq .SessionUser.Role "admin" }}
<hr/> <hr/>
<span><a href="users">Users</a></span> <span><a href="users">Users</a></span>
<span><a href="shares">Shares</a></span> <span><a href="shares">Shares</a></span>
{{ end }} {{ end }}
<hr/> <hr/>
<span><a href="logout">Logout</a></span> <span><a href="my-shares">My Shares</a></span>
{{ end }} <hr/>
</div> <span><a href="logout">Logout</a></span>
<div style="clear: both;"></div> {{ end }}
</body> </div>
<div style="clear: both;"></div>
</body>
</html> </html>

67
templates/my-shares.html Normal file
View File

@ -0,0 +1,67 @@
{{ define "page-styles"}}
<style>
div.share-user, div.share-login {
margin-left: 2em;
margin-top: .5em;
font-style: italic;
}
a.share-add-user, a.share-add-login {
margin-left: 2em;
margin-top: .5em;
font-style: italic;
}
div.share {
margin-bottom: 2em;
}
:target {
background-color: #c4e1ff;
}
</style>
{{ end }}
{{ define "page-content" }}
<div id="shares">
{{ range $share := .ShareInfos }}
<div id="share-{{$share.UUID}}" class="share">
UUID: {{ $share.UUID }} {{ if $share.IsAdmin }}
<form style="display: inline-block;" action="delete-share" method="post">
<input type="hidden" name="share" value="{{ $share.UUID }}"/>
<input type="submit" value="Delete" class="delete"/>
</form>{{ end }}
<br/>
Name: {{ $share.Name }}<br/>
<hr/>
{{ range $login := .Logins }}
<div id="login-{{$share.UUID}}-{{$login.LoginName}}" class="share-login">
Login {{ $login.LoginName }} {{ if $login.ReadOnly }}(ReadOnly){{ end }}
<form style="display: inline-block;" action="share-delete-login" method="post">
<input type="hidden" name="share" value="{{ $share.UUID }}"/>
<input type="hidden" name="login" value="{{ $login.LoginName }}"/>
<input type="submit" value="Delete" class="delete"/>
</form>
</div>
{{ end }}
<a href="share-add-login?share={{ $share.UUID }}" class="share-add-login">Add Login</a>
<hr/>
{{ if $share.IsAdmin }}
{{ range $user := .Users }}
<div id="user-{{$share.UUID}}-{{$user.Username}}" class="share-user">
User {{ $user.Username }} ({{ $user.Role }})
<form style="display: inline-block;" action="share-delete-user" method="post">
<input type="hidden" name="source" value="my-shares"/>
<input type="hidden" name="share" value="{{ $share.UUID }}"/>
<input type="hidden" name="user" value="{{ $user.Username }}"/>
<input type="submit" value="Delete" class="delete"/>
</form>
</div>
{{ end }}
{{ end }}
<a href="share-add-user?share={{ $share.UUID }}" class="share-add-user">Add User</a>
</div>
{{ end }}
<a href="create-share" class="create-share">Create Share</a>
</div>
{{ end }}

View File

@ -0,0 +1,15 @@
{{ define "page-content" }}
<form method="post">
<input name="share" type="hidden" value="{{ .ShareId }}"/>
<label>
Login: <input name="login" placeholder="Login name"/>
</label>
<label>
Password: <input type="password" name="password" placeholder="Password"/>
</label>
<label>
ReadOnly: <input type="checkbox" name="readonly"/>
</label>
<input type="submit" value="Add Login"/>
</form>
{{ end }}

View File

@ -1,47 +1,52 @@
{{ define "page-styles"}} {{ define "page-styles"}}
<style> <style>
div.share-user { div.share-user {
margin-left: 2em; margin-left: 2em;
margin-top: .5em; margin-top: .5em;
font-style: italic; font-style: italic;
} }
a.share-add-user {
margin-left: 2em; a.share-add-user {
margin-top: .5em; margin-left: 2em;
font-style: italic; margin-top: .5em;
} font-style: italic;
div.share { }
margin-bottom: 2em;
} div.share {
:target { margin-bottom: 2em;
background-color: #c4e1ff; }
}
</style> :target {
{{ end }} background-color: #c4e1ff;
}
{{ define "page-content" }} </style>
<div id="shares"> {{ end }}
{{ range $share := .ShareInfos }}
<div id="share-{{$share.UUID}}" class="share"> {{ define "page-content" }}
UUID: {{ $share.UUID }} <form style="display: inline-block;" action="delete-share" method="post"> <div id="shares">
<input type="hidden" name="share" value="{{ $share.UUID }}"/> {{ range $share := .ShareInfos }}
<input type="submit" value="Delete" class="delete"/> <div id="share-{{$share.UUID}}" class="share">
</form> UUID: {{ $share.UUID }}
<br/> <form style="display: inline-block;" action="delete-share" method="post">
Name: {{ $share.Name }}<br/> <input type="hidden" name="share" value="{{ $share.UUID }}"/>
{{ range $user := .Users }} <input type="submit" value="Delete" class="delete"/>
<div id="user-{{$share.UUID}}-{{$user.Username}}" class="share-user"> </form>
User {{ $user.Username }} ({{ $user.Role }}) <br/>
<form style="display: inline-block;" action="share-delete-user" method="post"> Name: {{ $share.Name }}<br/>
<input type="hidden" name="share" value="{{ $share.UUID }}"/> {{ range $user := .Users }}
<input type="hidden" name="user" value="{{ $user.Username }}"/> <div id="user-{{$share.UUID}}-{{$user.Username}}" class="share-user">
<input type="submit" value="Delete" class="delete"/> User {{ $user.Username }} ({{ $user.Role }})
</form> <form style="display: inline-block;" action="share-delete-user" method="post">
</div> <input type="hidden" name="source" value="shares"/>
{{ end }} <input type="hidden" name="share" value="{{ $share.UUID }}"/>
<a href="share-add-user?share={{ $share.UUID }}" class="share-add-user">Add User</a> <input type="hidden" name="user" value="{{ $user.Username }}"/>
</div> <input type="submit" value="Delete" class="delete"/>
{{ end }} </form>
<a href="create-share" class="create-share">Create Share</a> </div>
</div> {{ end }}
<a href="share-add-user?share={{ $share.UUID }}" class="share-add-user">Add User</a>
</div>
{{ end }}
<a href="create-share" class="create-share">Create Share</a>
</div>
{{ end }} {{ end }}

View File

@ -42,15 +42,18 @@ import (
) )
type webAdminHandler struct { type webAdminHandler struct {
router chi.Router router chi.Router
tplError *template.Template shareStore ShareStore
tplConfirm *template.Template tplError *template.Template
tplLogin *template.Template tplConfirm *template.Template
tplIndex *template.Template tplLogin *template.Template
tplUsers *template.Template tplIndex *template.Template
tplShares *template.Template tplUsers *template.Template
tplShareAddUser *template.Template tplShares *template.Template
tplCreateShare *template.Template tplMyShares *template.Template
tplShareAddUser *template.Template
tplShareAddLogin *template.Template
tplCreateShare *template.Template
} }
type sessionContext struct { type sessionContext struct {
@ -93,14 +96,17 @@ func newWebAdminHandler(app *app) *webAdminHandler {
} }
h := &webAdminHandler{ h := &webAdminHandler{
tplError: loadTemplate("error"), shareStore: app.shareStore,
tplConfirm: loadTemplate("confirm"), tplError: loadTemplate("error"),
tplLogin: loadSingleTemplate("login"), tplConfirm: loadTemplate("confirm"),
tplIndex: loadTemplate("index"), tplLogin: loadSingleTemplate("login"),
tplUsers: loadTemplate("users"), tplIndex: loadTemplate("index"),
tplShares: loadTemplate("shares"), tplUsers: loadTemplate("users"),
tplShareAddUser: loadTemplate("share-add-user"), tplShares: loadTemplate("shares"),
tplCreateShare: loadTemplate("create-share"), tplMyShares: loadTemplate("my-shares"),
tplShareAddUser: loadTemplate("share-add-user"),
tplShareAddLogin: loadTemplate("share-add-login"),
tplCreateShare: loadTemplate("create-share"),
} }
r := chi.NewRouter() r := chi.NewRouter()
@ -231,6 +237,54 @@ func newWebAdminHandler(app *app) *webAdminHandler {
"ShareInfos": shareInfos, "ShareInfos": shareInfos,
}) })
}) })
ar.Get("/my-shares", func(w http.ResponseWriter, r *http.Request) {
sessionContext := h.buildSessionContext(w, r)
shares, err := app.shareStore.FindSharesByUser(sessionContext.user.Username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
type shareInfo struct {
Share
IsAdmin bool
Logins []Login
Users []ShareUser
}
shareInfos := make([]shareInfo, len(shares))
for i := range shares {
shareInfos[i].Share = shares[i].Share
shareRole, err := app.shareStore.GetShareAccess(shares[i].Share, sessionContext.user.Username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
shareInfos[i].IsAdmin = sessionContext.user.Role == GlobalRoleAdmin ||
shareRole == ShareRoleAdmin
if shareInfos[i].IsAdmin {
users, err := app.shareStore.GetShareUsers(shares[i].Share)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
shareInfos[i].Users = users
}
logins, err := app.shareStore.GetShareLogins(shares[i].Share, sessionContext.user.Username)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
shareInfos[i].Logins = logins
}
sessionContext.RenderPage(h.tplMyShares, map[string]interface{}{
"ShareInfos": shareInfos,
})
})
ar.Route("/share-add-user", func(r chi.Router) { ar.Route("/share-add-user", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) { r.Get("/", func(w http.ResponseWriter, r *http.Request) {
sessionContext := h.buildSessionContext(w, r) sessionContext := h.buildSessionContext(w, r)
@ -241,12 +295,9 @@ func newWebAdminHandler(app *app) *webAdminHandler {
return return
} }
if sessionContext.user.Role != GlobalRoleAdmin { if !sessionContext.IsAdmin(share) {
shareRole, err := app.shareStore.GetShareAccess(share, sessionContext.user.Username) sessionContext.Unauthorized()
if err != nil || shareRole != ShareRoleAdmin { return
sessionContext.Unauthorized()
return
}
} }
users, err := app.userStore.GetUsers() users, err := app.userStore.GetUsers()
@ -271,12 +322,9 @@ func newWebAdminHandler(app *app) *webAdminHandler {
return return
} }
if sessionContext.user.Role != GlobalRoleAdmin { if !sessionContext.IsAdmin(share) {
shareRole, err := app.shareStore.GetShareAccess(share, sessionContext.user.Username) sessionContext.Unauthorized()
if err != nil || shareRole != ShareRoleAdmin { return
sessionContext.Unauthorized()
return
}
} }
user, err := app.userStore.GetUser(r.FormValue("user")) user, err := app.userStore.GetUser(r.FormValue("user"))
@ -300,6 +348,94 @@ func newWebAdminHandler(app *app) *webAdminHandler {
ar.Post("/share-delete-user", func(w http.ResponseWriter, r *http.Request) { ar.Post("/share-delete-user", func(w http.ResponseWriter, r *http.Request) {
sessionContext := h.buildSessionContext(w, r) sessionContext := h.buildSessionContext(w, r)
returnURL := r.FormValue("source")
share, err := app.shareStore.GetShare(r.FormValue("share"))
if err != nil {
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
return
}
if !sessionContext.IsAdmin(share) {
sessionContext.Unauthorized()
return
}
username := r.FormValue("user")
message := fmt.Sprintf(`You are about to delete the user &quot;%s&quot; from the share %s.<br/>
This will irrevocably also remove all logins of that user.<br/><br/>
Are you sure you want to continue?`, username, share.Name)
if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "share-delete-user"); confirmStatus == confirmRequested {
// We have already rendered. Nothing to do.
return
} else if confirmStatus == confirmAccepted {
err = app.shareStore.RemoveUserFromShare(share, username)
if err != nil {
sessionContext.RenderError(template.HTML("Cannot remove user from share: "+err.Error()), returnURL)
return
}
}
http.Redirect(w, r, returnURL, http.StatusFound)
})
ar.Route("/share-add-login", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
sessionContext := h.buildSessionContext(w, r)
share, err := app.shareStore.GetShare(r.URL.Query().Get("share"))
if err != nil {
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), "")
return
}
if !sessionContext.IsAdmin(share) {
sessionContext.Unauthorized()
return
}
sessionContext.RenderPage(h.tplShareAddLogin, map[string]interface{}{
"ShareId": share.UUID,
})
})
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
sessionContext := h.buildSessionContext(w, r)
returnURL := "share-add-login?share=" + r.FormValue("share")
share, err := app.shareStore.GetShare(r.FormValue("share"))
if err != nil {
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), returnURL)
return
}
if !sessionContext.IsAdmin(share) {
sessionContext.Unauthorized()
return
}
pwHash, err := bcrypt.GenerateFromPassword([]byte(r.FormValue("password")), bcrypt.DefaultCost)
if err != nil {
sessionContext.RenderError(template.HTML("Internal error: "+err.Error()), returnURL)
return
}
err = app.shareStore.AddLogin(share, sessionContext.user.Username, Login{
LoginName: r.FormValue("login"),
Password: string(pwHash),
ReadOnly: r.FormValue("readonly") == "on",
})
if err != nil {
sessionContext.RenderError(template.HTML("Cannot add login: "+err.Error()), returnURL)
return
}
sessionContext.Redirect("my-shares")
})
})
ar.Post("/share-delete-login", func(w http.ResponseWriter, r *http.Request) {
sessionContext := h.buildSessionContext(w, r)
returnURL := "shares" returnURL := "shares"
share, err := app.shareStore.GetShare(r.FormValue("share")) share, err := app.shareStore.GetShare(r.FormValue("share"))
@ -308,21 +444,27 @@ func newWebAdminHandler(app *app) *webAdminHandler {
return return
} }
if sessionContext.user.Role != GlobalRoleAdmin { if !sessionContext.IsAdmin(share) {
shareRole, err := app.shareStore.GetShareAccess(share, sessionContext.user.Username) sessionContext.Unauthorized()
if err != nil || shareRole != ShareRoleAdmin { return
sessionContext.Unauthorized() }
loginName := r.FormValue("login")
message := fmt.Sprintf(`You are about to delete the login &quot;%s&quot;.<br/><br/>
Are you sure you want to continue?`, loginName)
if confirmStatus := sessionContext.RequestConfirmation(template.HTML(message), "share-delete-login"); confirmStatus == confirmRequested {
// We have already rendered. Nothing to do.
return
} else if confirmStatus == confirmAccepted {
err = app.shareStore.RemoveLogin(share, sessionContext.user.Username, loginName)
if err != nil {
sessionContext.RenderError(template.HTML("Cannot remove login from share: "+err.Error()), returnURL)
return return
} }
} }
err = app.shareStore.RemoveUserFromShare(share, r.FormValue("user")) http.Redirect(w, r, "my-shares", http.StatusFound)
if err != nil {
sessionContext.RenderError(template.HTML("Cannot remove user from share: "+err.Error()), returnURL)
return
}
http.Redirect(w, r, "shares", http.StatusFound)
}) })
ar.Route("/create-share", func(r chi.Router) { ar.Route("/create-share", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) { r.Get("/", func(w http.ResponseWriter, r *http.Request) {
@ -358,12 +500,9 @@ func newWebAdminHandler(app *app) *webAdminHandler {
return return
} }
if sessionContext.user.Role != GlobalRoleAdmin { if !sessionContext.IsAdmin(share) {
shareRole, err := app.shareStore.GetShareAccess(share, sessionContext.user.Username) sessionContext.Unauthorized()
if err != nil || shareRole != ShareRoleAdmin { return
sessionContext.Unauthorized()
return
}
} }
message := fmt.Sprintf(`You are about to delete the share %s (%s).<br/> message := fmt.Sprintf(`You are about to delete the share %s (%s).<br/>
@ -429,6 +568,16 @@ func (s *sessionContext) RenderError(msg template.HTML, returnURL string) {
s.RenderPage(s.h.tplError, model) s.RenderPage(s.h.tplError, model)
} }
func (s *sessionContext) IsAdmin(share Share) bool {
if s.user.Role != GlobalRoleAdmin {
shareRole, err := s.h.shareStore.GetShareAccess(share, s.user.Username)
if err != nil || shareRole != ShareRoleAdmin {
return false
}
}
return true
}
func (s *sessionContext) Unauthorized() { func (s *sessionContext) Unauthorized() {
http.Error(s.w, http.StatusText(http.StatusForbidden), http.StatusForbidden) http.Error(s.w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
} }