update HTML UI to reflect role access restrictions

* disable ui if user is unelevated
* disable revoke button if unelevated and not own invite
* improve styling of disabled elements
* remove revoke if alias not made my current user
This commit is contained in:
cblgh 2021-04-21 13:52:44 +02:00 committed by Henry
parent 5dba245c16
commit 2c9fdcb98e
14 changed files with 167 additions and 63 deletions

View File

@ -133,6 +133,10 @@ func newSession(t *testing.T) *testSession {
testFuncs["urlToNotice"] = func(name string) string { return "" }
testFuncs["language_count"] = func() int { return 1 }
testFuncs["list_languages"] = func(*url.URL, string) string { return "" }
testFuncs["member_is_elevated"] = func() bool { return true }
testFuncs["member_is_admin"] = func() bool { return true }
testFuncs["member_can_invite"] = func() bool { return true }
testFuncs["list_languages"] = func(*url.URL, string) string { return "" }
testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) }
renderOpts := []render.Option{

View File

@ -123,6 +123,31 @@ func New(
}
}),
render.InjectTemplateFunc("member_can_invite", func(r *http.Request) interface{} {
return func() (bool, error) {
member := members.FromContext(r.Context())
if member == nil {
return false, nil
}
pm, err := dbs.Config.GetPrivacyMode(r.Context())
if err != nil {
return false, err
}
switch pm {
case roomdb.ModeOpen:
return true, nil
case roomdb.ModeCommunity:
return member.Role > roomdb.RoleUnknown && member.Role <= roomdb.RoleAdmin, nil
case roomdb.ModeRestricted:
return member.Role == roomdb.RoleAdmin || member.Role == roomdb.RoleModerator, nil
default:
return false, nil
}
}
}),
render.InjectTemplateFunc("language_count", func(r *http.Request) interface{} {
return func() int {
return len(locHelper.ListLanguages())

View File

@ -109,7 +109,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
}
// have the database return okay for any user
testUser := roomdb.Member{ID: 23}
testUser := roomdb.Member{ID: 23, Role: roomdb.RoleAdmin}
ts.AuthFallbackDB.CheckReturns(testUser.ID, nil)
ts.MembersDB.GetByIDReturns(testUser, nil)

View File

@ -117,6 +117,7 @@ AdminMemberDetailsRemove = "Mitglied entfernen"
AdminMemberAdded = "Mitglied erfolgreich hinzugefügt."
AdminMemberUpdated = "Mitglied aktualisiert."
AdminMemberRemoved = "Mitglied entfernt."
AdminAddNewMemberTitle = "Add a new member"
AdminAliasesRevoke = "Widerrufen"
AdminAliasesRevokeConfirmTitle = "Alias widerrufen"

View File

@ -124,6 +124,7 @@ AdminMemberDetailsRemove = "Remove member"
AdminMemberAdded = "Member added successfully."
AdminMemberUpdated = "Member updated."
AdminMemberRemoved = "Member removed."
AdminAddNewMemberTitle = "Add a new member"
AdminAliasesRevoke = "Revoke"
AdminAliasesRevokeConfirmTitle = "Revoke Alias"

View File

@ -92,13 +92,15 @@ func ContextInjecter(mdb roomdb.MembersService, withPassword *auth.Handler, with
}
// TemplateHelpers returns functions to be used with the go.mindeco.de/http/render package.
// Each has to return a function twice because the first is evaluated with the request before it gets passed onto html/template's FuncMap.
// Each helper has to return a function twice because the first is evaluated with the request before it gets passed onto html/template's FuncMap.
//
// {{ is_logged_in }} returns true or false depending if the user is logged in
// {{ is_logged_in }} returns true or false depending on if the user is logged in
//
// {{ member_has_role "string" }} returns a boolean which confrms wether the member has a certain role (RoleMemeber, RoleAdmin, etc)
//
// {{ member_is_admin }} is a shortcut for {{ member_has_role "RoleAdmin" }}
//
// {{ member_is_admin }} is a shortcut for {{ or member_has_role "RoleAdmin" member_has_role "RoleModerator"}}
func TemplateHelpers() []render.Option {
return []render.Option{
@ -143,5 +145,19 @@ func TemplateHelpers() []render.Option {
return member.Role == roomdb.RoleAdmin
}
}),
// shorthand for is admin || mod (used for editing notices, managing users, managing aliases)
render.InjectTemplateFunc("member_is_elevated", func(r *http.Request) interface{} {
no := func() bool { return false }
member := FromContext(r.Context())
if member == nil {
return no
}
return func() bool {
return member.Role == roomdb.RoleAdmin || member.Role == roomdb.RoleModerator
}
}),
}
}

View File

@ -22,21 +22,32 @@
{{ .csrfField }}
<div class="flex flex-row items-center h-12">
<input
{{ if member_is_elevated }} {{ else }} disabled {{ end }}
type="text"
name="pub_key"
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
class="font-mono truncate w-1/2 mr-2 tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
class="p-1 rounded font-mono truncate w-1/2 mr-2 tracking-wider h-12 shadow text-gray-900 focus:outline-none focus:ring-1
focus:ring-green-500 focus:border-transparent placeholder-gray-300
{{ if member_is_elevated }} {{ else }} shadow ring-1 ring-gray-300 opacity-50 bg-gray-200 cursor-not-allowed {{ end }}
"
>
<input
{{ if member_is_elevated }} {{ else }} disabled {{ end }}
type="text"
name="comment"
placeholder="{{i18n "AdminDeniedKeysComment"}}"
class="font-mono truncate w-1/2 tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
class="p-1 rounded font-mono truncate w-1/2 mr-2 tracking-wider h-12 shadow text-gray-900 focus:outline-none focus:ring-1
focus:ring-green-500 focus:border-transparent placeholder-gray-300
{{ if member_is_elevated }} {{ else }} shadow ring-1 ring-gray-300 opacity-50 bg-gray-200 cursor-not-allowed {{ end }}
"
>
<input
{{ if member_is_elevated }} {{ else }} disabled {{ end }}
type="submit"
value="{{i18n "AdminDeniedKeysAdd"}}"
class="pl-4 w-20 py-2 text-center text-green-500 hover:text-green-600 font-bold bg-transparent cursor-pointer"
class="pl-4 w-20 py-2 text-center font-bold bg-transparent disabled:opacity-50
{{ if member_is_elevated }} text-green-500 hover:text-green-600 cursor-pointer {{ else }} text-gray-200 cursor-not-allowed {{ end }}
"
>
</div>
</form>
@ -98,4 +109,4 @@
{{end}}
</div>
{{end}}
{{end}}
{{end}}

View File

@ -28,8 +28,15 @@
>
{{ .csrfField }}
<button
{{ if not member_can_invite }} disabled {{ end }}
type="submit"
class="shadow rounded px-3 py-1.5 text-green-600 ring-1 ring-green-400 bg-white hover:bg-green-500 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-green-400"
class="shadow rounded px-3 py-1.5 ring-1 focus:outline-none focus:ring-2
{{ if member_can_invite }}
text-green-600 ring-green-400 bg-white hover:bg-green-500 hover:text-gray-100 focus:ring-green-400
{{ else }}
text-gray-500 ring-gray-200 bg-gray-300 cursor-not-allowed
{{ end }}
"
>{{i18n "AdminInvitesCreate"}}</button>
</form>
</td>
@ -44,6 +51,8 @@
<tbody id="the-table-rows" class="divide-y">
{{range .Entries}}
{{ $user := is_logged_in }}
{{$hasCreatedInvite := eq $user.PubKey.Ref .CreatedBy.PubKey.Ref }}
{{$creator := .CreatedBy.PubKey.Ref}}
{{$creatorIsAlias := false}}
{{range $index, $alias := .CreatedBy.Aliases}}
@ -67,10 +76,12 @@
{{end}}
</td>
<td class="w-3/12 pl-2 pr-3 text-right">
{{ if or member_is_elevated $hasCreatedInvite }}
<a
href="{{urlTo "admin:invites:revoke:confirm" "id" .ID}}"
class="pl-2 w-20 py-2 text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>{{i18n "AdminInviteRevoke"}}</a>
{{ end }}
</td>
</tr>
<tr class="h-12 table-row sm:hidden">
@ -84,10 +95,12 @@
>{{$creator}}</span>, {{human_time .CreatedAt}}
{{end}}
</span>
<a
href="{{urlTo "admin:invites:revoke:confirm" "id" .ID}}"
class="pl-4 w-20 py-2 text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>{{i18n "AdminInviteRevoke"}}</a>
{{ if or member_is_elevated $hasCreatedInvite }}
<a
href="{{urlTo "admin:invites:revoke:confirm" "id" .ID}}"
class="pl-4 w-20 py-2 text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>{{i18n "AdminInviteRevoke"}}</a>
{{ end }}
</td>
</tr>
{{end}}
@ -134,4 +147,4 @@
{{end}}
</div>
{{end}}
{{end}}
{{end}}

View File

@ -14,16 +14,23 @@
method="POST"
>
{{ .csrfField }}
<label class="block mt-6 mb-1 text-sm text-gray-500">Add a new member</label>
<label class="block mt-6 mb-1 text-sm text-gray-500">{{ i18n "AdminAddNewMemberTitle" }}</label>
<div class="flex flex-row items-center justify-start mb-6">
<input
{{ if member_is_elevated }} {{ else }} disabled {{ end }}
type="text"
name="pub_key"
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
class="w-8/12 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate text-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
class="w-8/12 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate
text-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent
{{ if member_is_elevated }} {{ else }} shadow ring-1 ring-gray-300 opacity-50 bg-gray-200 cursor-not-allowed {{ end }}
">
<button
{{ if member_is_elevated }} {{ else }} disabled {{ end }}
type="submit"
class="ml-4 h-10 shadow rounded px-6 text-purple-600 ring-1 ring-purple-400 bg-white hover:bg-purple-600 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-purple-400"
class="ml-4 h-10 shadow rounded px-6 text-purple-600 ring-1 bg-white focus:outline-none focus:ring-2 focus:ring-purple-400
{{ if member_is_elevated }} ring-purple-400 hover:bg-purple-600 hover:text-gray-100 {{ else }} opacity-50 ring-purple-300 bg-purple-200 cursor-not-allowed {{ end }}
"
>{{i18n "AdminMembersAdd"}}</button>
</div>
</form>
@ -106,4 +113,4 @@
{{end}}
</div>
{{end}}
{{end}}
{{end}}

View File

@ -8,45 +8,55 @@
<p id="ssb-id" class="mb-8 font-mono font-bold tracking-wider truncate text-gray-900">{{.Member.PubKey.Ref}}</p>
<label class="mt-2 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsRole"}}</label>
<details class="mb-8 self-start w-40" id="change-role">
<summary class="px-3 py-1 rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
{{ $user := is_logged_in }}
{{ $aliasBelongsToUser := eq $user.PubKey.Ref .Member.PubKey.Ref }}
{{ if member_is_elevated }}
<details class="mb-8 self-start w-40" id="change-role">
<summary class="px-3 py-1 rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
{{range $.AllRoles}}
{{if eq . $.Member.Role}}
{{i18n .String}}
{{end}}
{{end}}
</summary>
<div class="absolute z-10 bg-white w-40 mt-2 shadow-xl ring-1 ring-gray-200 rounded divide-y flex flex-col items-stretch overflow-hidden">
{{range $.AllRoles}}
{{if ne . $.Member.Role}}
<form
action="{{urlTo "admin:members:change-role" "id" $.Member.ID}}"
method="POST"
>
{{$.csrfField}}
<input type="hidden" name="role" value="{{.}}">
<input
type="submit"
value="{{i18n .String}}"
class="pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer"
/>
</form>
{{else}}
<div
class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default"
>
<div class="w-10 flex flex-row items-center justify-center">
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
</div>
<span>{{i18n .String}}</span>
</div>
{{end}}
{{end}}
</div>
</details>
{{ else }}
{{range $.AllRoles}}
{{if eq . $.Member.Role}}
{{i18n .String}}
{{end}}
<p id="ssb-member-role" class="mb-8 font-bold tracking-wider truncate text-gray-900">{{i18n .String}}</p>
{{end}}
{{end}}
</summary>
<div class="absolute z-10 bg-white w-40 mt-2 shadow-xl ring-1 ring-gray-200 rounded divide-y flex flex-col items-stretch overflow-hidden">
{{range $.AllRoles}}
{{if ne . $.Member.Role}}
<form
action="{{urlTo "admin:members:change-role" "id" $.Member.ID}}"
method="POST"
>
{{$.csrfField}}
<input type="hidden" name="role" value="{{.}}">
<input
type="submit"
value="{{i18n .String}}"
class="pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer"
/>
</form>
{{else}}
<div
class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default"
>
<div class="w-10 flex flex-row items-center justify-center">
<svg class="w-4 h-4" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
</div>
<span>{{i18n .String}}</span>
</div>
{{end}}
{{end}}
</div>
</details>
{{ end }}
{{$aliasCount := len .Member.Aliases}} {{if gt $aliasCount 0}}
<label class="mt-2 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsAliases"}}</label>
@ -59,19 +69,23 @@
>{{.Name}}</a>
</div>
<a
href="{{urlTo "admin:aliases:revoke:confirm" "id" .ID}}"
class="w-20 py-2 text-sm text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
{{ if or member_is_elevated $aliasBelongsToUser }}
<a
href="{{urlTo "admin:aliases:revoke:confirm" "id" .ID}}"
class="w-20 py-2 text-sm text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>({{i18n "AdminMemberDetailsAliasRevoke"}})</a>
{{end}}
{{end}}
</div>
{{end}}
{{ if member_is_elevated }}
<label class="mt-10 mb-1 font-bold text-gray-400 text-sm">{{i18n "AdminMemberDetailsExclusion"}}</label>
<a
id="remove-member"
href="{{urlTo "admin:members:remove:confirm" "id" .Member.ID}}"
class="mb-8 self-start shadow rounded px-3 py-1 text-red-600 ring-1 ring-red-400 bg-white hover:bg-red-600 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-red-400 cursor-pointer"
>{{i18n "AdminMemberDetailsRemove"}}</a>
{{ end }}
{{end}}
{{end}}

View File

@ -11,6 +11,7 @@
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
</p>
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetPrivacyModeTitle" }}</h3>
{{ if member_is_admin }}
<details class="mb-8 self-start max-w-sm" id="change-privacy">
<summary class="px-3 py-1 max-w-sm rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
{{ i18n .CurrentMode.String }}
@ -44,6 +45,11 @@
{{end}}
</div>
</details>
{{ else }}
<input disabled type="text" value="{{ i18n $.CurrentMode.String }}"
class="mb-8 self-start max-w-sm px-3 py-1 max-w-sm rounded shadow ring-1 ring-gray-300 bg-gray-200 opacity-50 cursor-not-allowed"
>
{{ end }}
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
@ -59,6 +65,7 @@
{{ i18n "ExplanationDefaultLanguage" }}
</p>
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetDefaultLanguageTitle" }}</h3>
{{ if member_is_admin }}
<details class="mb-8 self-start max-w-sm">
<summary id="language-summary" class="px-3 py-1 max-w-sm rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
{{ $.CurrentLanguage }}
@ -68,6 +75,11 @@
{{ list_languages $adminSetLanguageUrl "pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer" }}
</div>
</details>
{{ else }}
<input disabled type="text" value="{{ $.CurrentLanguage }}"
class="mb-8 self-start max-w-sm px-3 py-1 max-w-sm rounded shadow ring-1 ring-gray-300 bg-gray-200 opacity-50 cursor-not-allowed"
>
{{ end }}
</div>
</div>

View File

@ -9,11 +9,11 @@
</div>
<div class="h-8"></div>
{{if is_logged_in}}
{{if and is_logged_in member_is_elevated }}
<a
id="edit-notice"
href="{{urlTo "admin:notice:edit" "id" .ID}}"
class="self-start shadow rounded px-4 h-8 flex flex-row justify-center items-center text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
>{{i18n "NoticeEditTitle"}}</a>
{{end}}
{{end}}
{{end}}

View File

@ -20,7 +20,7 @@
>{{.Language}}</a>
{{end}}
{{if is_logged_in}}
{{if and is_logged_in member_is_elevated }}
<a
href="{{urlTo "admin:notice:translation:draft" "name" .Name.String}}"
class="inline-block rounded-full py-1 px-3 text-sm text-gray-500 font-bold border-2 border-dashed box-content border-gray-200 hover:border-transparent hover:bg-white hover:shadow"
@ -29,4 +29,4 @@
</div>
{{end}}
</div>
{{end}}
{{end}}

View File

@ -12,11 +12,11 @@
</div>
<div class="h-8"></div>
{{if is_logged_in}}
{{if and is_logged_in member_is_elevated }}
<a
id="edit-notice"
href="{{urlTo "admin:notice:edit" "id" .ID}}"
class="self-start shadow rounded px-4 h-8 flex flex-row justify-center items-center text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
>{{i18n "NoticeEditTitle"}}</a>
{{end}}
{{end}}
{{end}}