Merge pull request #179 from ssb-ngi-pointer/disable-ui-if-unelevated

disable ui if user is unelevated
This commit is contained in:
Alexander Cobleigh 2021-04-23 08:50:58 +02:00 committed by GitHub
commit 5e9dc63153
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 485 additions and 76 deletions

View File

@ -8,6 +8,7 @@ import (
"net/url"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
@ -32,6 +33,85 @@ func TestDeniedKeysEmpty(t *testing.T) {
})
}
func TestDeniedKeysDisabledInterface(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
listURL := ts.URLTo(router.AdminDeniedKeysOverview)
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
html, resp := ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
formSelection := html.Find("form#add-entry")
a.EqualValues(1, formSelection.Length())
method, ok := formSelection.Attr("method")
a.True(ok, "form has method set")
a.Equal("POST", method)
action, ok := formSelection.Attr("action")
a.True(ok, "form has action set")
addURL := ts.URLTo(router.AdminDeniedKeysAdd)
a.Equal(addURL.String(), action)
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "pub_key", Type: "text"},
{Name: "comment", Type: "text"},
})
newKey := "@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519"
addVals := url.Values{
"comment": []string{"some comment"},
// just any key that looks valid
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(1, ts.DeniedKeysDB.AddCallCount())
_, addedKey, addedComment := ts.DeniedKeysDB.AddArgsForCall(0)
a.Equal(newKey, addedKey.Ref())
a.Equal("some comment", addedComment)
/* Verify that the inputs are visible/hidden depending on user roles */
checkInputsAreDisabled := func(shouldBeDisabled bool) {
html, resp = ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
inputContainer := html.Find("#denied-keys-input-container")
a.Equal(1, inputContainer.Length())
inputs := inputContainer.Find("input")
// pubkey, comment, submit button
a.Equal(3, inputs.Length())
inputs.Each(func(i int, el *goquery.Selection) {
_, disabled := el.Attr("disabled")
a.Equal(shouldBeDisabled, disabled)
})
}
// verify that inputs are enabled for RoleAdmin
checkInputsAreDisabled(false)
// verify that inputs are enabled for RoleModerator
ts.User = roomdb.Member{
ID: 9001,
Role: roomdb.RoleModerator,
}
checkInputsAreDisabled(false)
// verify that inputs are disabled for RoleMember
ts.User = roomdb.Member{
ID: 7331,
Role: roomdb.RoleMember,
}
checkInputsAreDisabled(true)
}
func TestDeniedKeysAdd(t *testing.T) {
ts := newSession(t)
a := assert.New(t)

View File

@ -64,6 +64,50 @@ func TestInvitesOverview(t *testing.T) {
a.True(yes, "a-tag has href attribute")
wantURL := ts.URLTo(router.AdminInvitesRevokeConfirm, "id", 666)
a.Equal(wantURL.String(), link)
testInviteButtonDisabled := func(shouldBeDisabled bool) {
html, resp = ts.Client.GetHTML(invitesOverviewURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
inviteButton := html.Find("#create-invite button")
_, disabled := inviteButton.Attr("disabled")
a.EqualValues(shouldBeDisabled, disabled, "invite button should be disabled")
}
// member, mod, admin should all be able to invite in ModeCommunity
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
testInviteButtonDisabled(false)
ts.User = roomdb.Member{
ID: 7331,
Role: roomdb.RoleModerator,
}
testInviteButtonDisabled(false)
ts.User = roomdb.Member{
ID: 9001,
Role: roomdb.RoleMember,
}
testInviteButtonDisabled(false)
// mod and admin should be able to invite, member should not
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeRestricted, nil)
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
testInviteButtonDisabled(false)
ts.User = roomdb.Member{
ID: 7331,
Role: roomdb.RoleModerator,
}
testInviteButtonDisabled(false)
ts.User = roomdb.Member{
ID: 9001,
Role: roomdb.RoleMember,
}
testInviteButtonDisabled(true)
}
func TestInvitesCreateForm(t *testing.T) {

View File

@ -7,6 +7,7 @@ import (
"net/url"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
@ -35,6 +36,11 @@ func TestMembersAdd(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
listURL := ts.URLTo(router.AdminMembersOverview)
html, resp := ts.Client.GetHTML(listURL)
@ -70,6 +76,43 @@ func TestMembersAdd(t *testing.T) {
a.Equal(newKey, addedPubKey.Ref())
a.Equal(roomdb.RoleMember, addedRole)
/* Verify that the inputs are visible/hidden depending on user roles */
checkInputsAreDisabled := func(shouldBeDisabled bool) {
html, resp = ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
inputContainer := html.Find("#add-member-input-container")
a.Equal(1, inputContainer.Length())
inputs := inputContainer.Find("input")
// pubkey
a.Equal(1, inputs.Length())
inputs.Each(func(i int, el *goquery.Selection) {
_, disabled := el.Attr("disabled")
a.Equal(shouldBeDisabled, disabled)
})
button := inputContainer.Find("button")
a.Equal(1, button.Length())
button.Each(func(i int, el *goquery.Selection) {
_, disabled := el.Attr("disabled")
a.Equal(shouldBeDisabled, disabled)
})
}
// verify that inputs are enabled for RoleAdmin
checkInputsAreDisabled(false)
// verify that inputs are enabled for RoleModerator
ts.User = roomdb.Member{
ID: 9001,
Role: roomdb.RoleModerator,
}
checkInputsAreDisabled(false)
// verify that inputs are disabled for RoleMember
ts.User = roomdb.Member{
ID: 7331,
Role: roomdb.RoleMember,
}
checkInputsAreDisabled(true)
}
func TestMembersDontAddInvalid(t *testing.T) {
@ -173,6 +216,11 @@ func TestMemberDetails(t *testing.T) {
memberURL := ts.URLTo(router.AdminMemberDetails, "id", "1")
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
html, resp := ts.Client.GetHTML(memberURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
@ -180,14 +228,6 @@ func TestMemberDetails(t *testing.T) {
{"title", "AdminMemberDetailsTitle"},
})
// check for SSB ID
ssbID := html.Find("#ssb-id")
a.Equal(feedRef.Ref(), ssbID.Text())
// check for change-role dropdown
roleDropdown := html.Find("#change-role")
a.EqualValues(roleDropdown.Length(), 1)
aliasList := html.Find("#alias-list").Find("a")
// check for link to resolve 1st Alias
@ -220,6 +260,37 @@ func TestMemberDetails(t *testing.T) {
a.True(yes, "a-tag has href attribute")
wantLink = ts.URLTo(router.AdminMembersRemoveConfirm, "id", 1)
a.Equal(wantLink.String(), removeLink)
testDisabledBehaviour := func(isElevated bool) {
html, resp := ts.Client.GetHTML(memberURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
// check for SSB ID
ssbID := html.Find("#ssb-id")
a.Equal(feedRef.Ref(), ssbID.Text())
// check for change-role dropdown
roleDropdown := html.Find("#change-role")
if isElevated {
a.Equal(1, roleDropdown.Length())
} else {
a.Equal(0, roleDropdown.Length())
}
}
testDisabledBehaviour(true)
/* Now: verify that moderators cannot make room settings changes */
ts.User = roomdb.Member{
ID: 7331,
Role: roomdb.RoleModerator,
}
testDisabledBehaviour(true)
/* Finally: verify that members cannot make room settings changes */
ts.User = roomdb.Member{
ID: 9001,
Role: roomdb.RoleMember,
}
testDisabledBehaviour(false)
}
func TestMembersRemoveConfirmation(t *testing.T) {

View File

@ -5,6 +5,7 @@ import (
"strings"
"testing"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/stretchr/testify/assert"
)
@ -33,6 +34,10 @@ func TestLanguageSetDefaultLanguage(t *testing.T) {
a := assert.New(t)
ts.ConfigDB.GetDefaultLanguageReturns("de", nil)
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
u := ts.URLTo(router.AdminSettings)
html, resp := ts.Client.GetHTML(u)

View File

@ -0,0 +1,100 @@
package admin
import (
"net/http"
"strings"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
func TestSettingsOverview(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
/* First: make sure everything renders correctly for admins */
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleAdmin,
}
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
ts.ConfigDB.GetDefaultLanguageReturns("en", nil)
settingsURL := ts.URLTo(router.AdminSettings)
html, resp := ts.Client.GetHTML(settingsURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
// the privacy mode form & its summary/details container should exist
privacyFormContainer := html.Find("#change-privacy")
a.Equal(1, privacyFormContainer.Length())
a.Equal(1, privacyFormContainer.Find("summary").Length())
// chosen privacy mode is ModeCommunity (english translation will only be the name of the label, due to testing suite is set up atm)
a.Equal("ModeCommunity", strings.TrimSpace(privacyFormContainer.Find("summary").Text()))
// details-dropdown should have two forms, one for each of the other two privacy modes
// that can be selected (ModeOpen, ModeRestricted)
a.Equal(2, privacyFormContainer.Find("form").Length())
// and one span, showing the selected mode
a.Equal(1, privacyFormContainer.Find("#selected-mode").Length())
inputs := privacyFormContainer.Find("input")
// verify none of the privacy mode container's inputs are disabled
inputs.Each(func(i int, el *goquery.Selection) {
_, exists := el.Attr("disabled")
a.False(exists)
})
// verify that the change language form exists & is enabled
languageFormContainer := html.Find("#change-language-container")
a.Equal(1, languageFormContainer.Length())
a.Equal(1, languageFormContainer.Find("summary").Length())
// (english translation will only be the name of the label, due to testing suite is set up atm)
a.Equal("LanguageName", strings.TrimSpace(languageFormContainer.Find("summary").Text()))
testDisabledBehaviour := func() {
settingsURL := ts.URLTo(router.AdminSettings)
html, resp := ts.Client.GetHTML(settingsURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
// we do not have the summary/details hack if the forms are hidden
privacyFormContainer := html.Find("#change-privacy")
a.Equal(0, privacyFormContainer.Length())
// the should still be the parent container, however
privacyContainer := html.Find("#privacy-mode-container")
a.Equal(1, privacyContainer.Length())
// there should only be one input in the privacy mode container now
inputs := privacyContainer.Find("input")
a.Equal(1, inputs.Length())
// the input should be disabled
_, disabled := inputs.Attr("disabled")
a.True(disabled)
// next, verify that the change language setting is disabled
languageContainer := html.Find("#change-language-container")
a.Equal(1, languageContainer.Length())
// there should only be one input in the language mode container now
inputs = languageContainer.Find("input")
a.Equal(1, inputs.Length())
// the input should be disabled
_, disabled = inputs.Attr("disabled")
a.True(disabled)
}
/* Now: verify that moderators cannot make room settings changes */
ts.User = roomdb.Member{
ID: 7331,
Role: roomdb.RoleModerator,
}
testDisabledBehaviour()
/* Finally: verify that members cannot make room settings changes */
ts.User = roomdb.Member{
ID: 9001,
Role: roomdb.RoleMember,
}
testDisabledBehaviour()
}

View File

@ -133,6 +133,15 @@ 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 ts.User.Role == roomdb.RoleAdmin || ts.User.Role == roomdb.RoleModerator }
testFuncs["member_is_admin"] = func() bool { return ts.User.Role == roomdb.RoleAdmin }
testFuncs["member_can_invite"] = func() bool {
pm, _ := ts.ConfigDB.GetPrivacyMode(ctx)
memberElevated := ts.User.Role == roomdb.RoleAdmin || ts.User.Role == roomdb.RoleModerator
memberCanInvite := ts.User.Role == roomdb.RoleMember && (pm == roomdb.ModeCommunity || pm == roomdb.ModeOpen)
return memberElevated || memberCanInvite
}
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_elevated }} 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

@ -20,23 +20,34 @@
method="POST"
>
{{ .csrfField }}
<div class="flex flex-row items-center h-12">
<div id="denied-keys-input-container" 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>
<div class="flex flex-row items-center justify-start mb-6">
<label class="block mt-6 mb-1 text-sm text-gray-500">{{ i18n "AdminAddNewMemberTitle" }}</label>
<div id="add-member-input-container" 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

@ -4,13 +4,14 @@
class="text-3xl tracking-tight font-black text-black mt-2 mb-0"
>{{ i18n "Settings" }}</h1>
<div class="max-w-2xl">
<div class="max-w-2xl" id="privacy-mode-container">
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
<p class="mb-4">
{{ i18n "ExplanationPrivacyModes" }}
<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 }}
@ -38,12 +39,17 @@
<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>
<span id="selected-mode">{{ i18n .String }}</span>
</div>
{{end}}
{{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>
@ -53,12 +59,13 @@
<div class="text-md col-span-2 italic">{{ i18n "ExplanationRestricted" }}</div>
</div>
</div>
<div class="max-w-2xl">
<div class="max-w-2xl" id="change-language-container">
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "DefaultLanguageTitle" }}</h2>
<p class="mb-4">
{{ 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}}