diff --git a/roomdb/interface.go b/roomdb/interface.go index 92f8940..37e2130 100644 --- a/roomdb/interface.go +++ b/roomdb/interface.go @@ -140,7 +140,7 @@ type AliasesService interface { // InvitesService manages creation and consumption of invite tokens for joining the room. type InvitesService interface { // Create creates a new invite for a new member. It returns the token or an error. - // createdBy is user ID of the admin or moderator who created it. + // createdBy is user ID of the admin or moderator who created it. MemberID -1 is allowed if Privacy Mode is set to Open. // aliasSuggestion is optional (empty string is fine) but can be used to disambiguate open invites. (See https://github.com/ssb-ngi-pointer/rooms2/issues/21) Create(ctx context.Context, createdBy int64) (string, error) diff --git a/roomdb/sqlite/invites.go b/roomdb/sqlite/invites.go index f50c89e..82f5820 100644 --- a/roomdb/sqlite/invites.go +++ b/roomdb/sqlite/invites.go @@ -42,6 +42,27 @@ func (i Invites) Create(ctx context.Context, createdBy int64) (string, error) { err := transact(i.db, func(tx *sql.Tx) error { + if createdBy == -1 { + config, err := models.FindConfig(ctx, tx, configRowID) + if err != nil { + return err + } + + if config.PrivacyMode != roomdb.ModeOpen { + return fmt.Errorf("roomdb: privacy mode not set to open but %s", config.PrivacyMode.String()) + } + + m, err := models.Members(qm.Where("role = ?", roomdb.RoleAdmin)).One(ctx, tx) + if err != nil { + // we could insert something like a system user but should probably hit it from the members list then + if errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("roomdb: no admin user available to associate invite to") + } + return err + } + newInvite.CreatedBy = m.ID + } + inserted := false trying: for tries := 100; tries > 0; tries-- { @@ -68,7 +89,7 @@ func (i Invites) Create(ctx context.Context, createdBy int64) (string, error) { } if !inserted { - return errors.New("admindb: failed to generate an invite token in a reasonable amount of time") + return errors.New("roomdb: failed to generate an invite token in a reasonable amount of time") } return nil @@ -139,7 +160,7 @@ func (i Invites) Consume(ctx context.Context, token string, newMember refs.FeedR func deleteConsumedInvites(tx boil.ContextExecutor) error { _, err := models.Invites(qm.Where("active = false")).DeleteAll(context.Background(), tx) if err != nil { - return fmt.Errorf("admindb: failed to delete used invites: %w", err) + return fmt.Errorf("roomdb: failed to delete used invites: %w", err) } return nil } @@ -269,7 +290,7 @@ func getHashedToken(b64tok string) (string, error) { } if n := len(tokenBytes); n != inviteTokenLength { - return "", fmt.Errorf("admindb: invalid invite token length (only got %d bytes)", n) + return "", fmt.Errorf("roomdb: invalid invite token length (only got %d bytes)", n) } // hash the binary of the passed token diff --git a/web/errors/errhandler.go b/web/errors/errhandler.go index eed5372..62a7712 100644 --- a/web/errors/errhandler.go +++ b/web/errors/errhandler.go @@ -116,30 +116,21 @@ func localizeError(ih *i18n.Localizer, err error) (int, template.HTML) { msg = ih.LocalizeSimple("ErrorNotFound") case errors.As(err, &aa): - msg = ih.LocalizeWithData("ErrorAlreadyAdded", map[string]string{ - "Feed": aa.Ref.Ref(), - }) + msg = ih.LocalizeWithData("ErrorAlreadyAdded", "Feed", aa.Ref.Ref()) case errors.As(err, &pnf): code = http.StatusNotFound - msg = ih.LocalizeWithData("ErrorPageNotFound", map[string]string{ - "Path": pnf.Path, - }) + msg = ih.LocalizeWithData("ErrorPageNotFound", "Path", pnf.Path) case errors.As(err, &br): code = http.StatusBadRequest // TODO: we could localize all the "Where:" as labels, too // buttt it feels like overkill right now - msg = ih.LocalizeWithData("ErrorBadRequest", map[string]string{ - "Where": br.Where, - "Details": br.Details.Error(), - }) + msg = ih.LocalizeWithData("ErrorBadRequest", "Where", br.Where, "Details", br.Details.Error()) case errors.As(err, &f): code = http.StatusForbidden - msg = ih.LocalizeWithData("ErrorForbidden", map[string]string{ - "Details": f.Details.Error(), - }) + msg = ih.LocalizeWithData("ErrorForbidden", "Details", f.Details.Error()) } return code, msg diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go index 86b2a3a..de7134f 100644 --- a/web/handlers/admin/handler.go +++ b/web/handlers/admin/handler.go @@ -147,8 +147,6 @@ func Handler( db: dbs.Invites, config: dbs.Config, - - domainName: netInfo.Domain, } mux.HandleFunc("/invites", r.HTML("admin/invite-list.tmpl", ih.overview)) diff --git a/web/handlers/admin/invites.go b/web/handlers/admin/invites.go index a5d492d..12608c3 100644 --- a/web/handlers/admin/invites.go +++ b/web/handlers/admin/invites.go @@ -23,8 +23,6 @@ type invitesHandler struct { db roomdb.InvitesService config roomdb.RoomConfig - - domainName string } func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) { @@ -73,8 +71,6 @@ func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interf } facadeURL := h.urlTo(router.CompleteInviteFacade, "token", token) - facadeURL.Host = h.domainName - facadeURL.Scheme = "https" return map[string]interface{}{ "FacadeURL": facadeURL.String(), diff --git a/web/handlers/admin/setup_test.go b/web/handlers/admin/setup_test.go index b282acc..99c1812 100644 --- a/web/handlers/admin/setup_test.go +++ b/web/handlers/admin/setup_test.go @@ -144,6 +144,14 @@ 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["privacy_mode_is"] = func(mode string) bool { + pm, err := ts.ConfigDB.GetPrivacyMode(context.TODO()) + if err != nil { + t.Fatal(err) + } + + return pm.String() == mode + } 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"] = func(what string) (bool, error) { diff --git a/web/handlers/http.go b/web/handlers/http.go index 2581ca8..e5a36b4 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -110,6 +110,16 @@ func New( render.SetErrorHandler(eh.Handle), render.FuncMap(web.TemplateFuncs(m, netInfo)), + render.InjectTemplateFunc("privacy_mode_is", func(r *http.Request) interface{} { + return func(want string) bool { + has, err := dbs.Config.GetPrivacyMode(r.Context()) + if err != nil { + return false + } + return has.String() == want + } + }), + render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} { return func(routeName string) bool { route := m.Get(routeName) @@ -351,6 +361,7 @@ func New( m.Get(router.CompleteInviteFacadeFallback).Handler(r.HTML("invite/facade-fallback.tmpl", ih.presentFacadeFallback)) m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert)) m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume) + m.Get(router.OpenModeCreateInvite).HandlerFunc(r.HTML("admin/invite-created.tmpl", ih.createOpenMode)) // static assets m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets))) diff --git a/web/handlers/invites.go b/web/handlers/invites.go index 44f6a44..daf59c8 100644 --- a/web/handlers/invites.go +++ b/web/handlers/invites.go @@ -345,3 +345,18 @@ func (html inviteConsumeHTMLResponder) SendSuccess() { func (html inviteConsumeHTMLResponder) SendError(err error) { html.renderer.Error(html.rw, html.req, http.StatusInternalServerError, err) } + +func (h inviteHandler) createOpenMode(rw http.ResponseWriter, req *http.Request) (interface{}, error) { + ctx := req.Context() + + token, err := h.invites.Create(ctx, -1) + if err != nil { + return nil, err + } + + facadeURL := h.urlTo(router.CompleteInviteFacade, "token", token) + + return map[string]interface{}{ + "FacadeURL": facadeURL.String(), + }, nil +} diff --git a/web/i18n/defaults/active.de.toml b/web/i18n/defaults/active.de.toml index 3845adf..750d885 100644 --- a/web/i18n/defaults/active.de.toml +++ b/web/i18n/defaults/active.de.toml @@ -161,14 +161,14 @@ AdminInviteSuggestedAliasIs = "Der vorgeschlagene Alias ​​lautet:" AdminInviteSuggestedAliasIsShort = "Alias:" AdminInviteCreatedTitle = "Einladung erfolgreich erstellt!" -AdminInviteCreatedInstruct = "Kopieren Sie nun den folgenden Link und fügen Sie ihn in einen Freund ein, den Sie in diesen Raum einladen möchten." +AdminInviteCreatedInstruct = "Kopieren Sie nun den folgenden Link und geben Sie ihn an den Entfänger weiter, den Sie in diesen Raum einladen möchten." # public invites ################ InviteFacade = "Raum betreten" InviteFacadeTitle = "Raum betreten" -InviteFacadeWelcome = "Sie haben die Erlaubnis, Mitglied dieses Raums zu werden, weil jemand diese Einladung mit Ihnen geteilt hat." +InviteFacadeWelcome = "Willkommen bei {{.RoomTitle}}! Sie haben die Erlaubnis, Mitglied dieses Raums zu werden, weil jemand diese Einladung mit Ihnen geteilt hat." InviteFacadeInstruct = "Um die Einladung zu erhalten, klicken Sie auf die Schaltfläche unten, um eine kompatible SSB-App zu öffnen, falls diese installiert ist." InviteFacadeJoin = "Tritt diesem Raum bei" InviteFacadeWaiting = "SSB App öffnen" @@ -183,6 +183,13 @@ InviteInsertWelcome = "Sie können Ihre Einladung anfordern, indem Sie unten Ihr InviteConsumedTitle = "Einladung angenommen!" InviteConsumedWelcome = "Sie sind jetzt Mitglied dieses Raums. Wenn Sie eine Multiserver-Adresse benötigen, um eine Verbindung zum Raum herzustellen, können Sie die folgende kopieren und einfügen:" +# ssb uri links +############### + +SSBURIOpening = "SSB App öffnen " +SSBURIFailureWelcome = "Sind Sie neu bei SSB? Es scheint, dass Sie keine SSB-App haben, die diesen Link verstehen kann. Sie können eine dieser Apps installieren: " +SSBURIFailureInstallManyverse = "Manyverse Installieren" + # notices (mini-CMS) #################### diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index 8a1cfe0..0718432 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -175,7 +175,7 @@ AdminInviteCreatedInstruct = "Now, copy the link below and paste it to a friend InviteFacade = "Join Room" InviteFacadeTitle = "Join Room" -InviteFacadeWelcome = "You have permission to become a member of this room because someone has shared this invite with you." +InviteFacadeWelcome = "Welcome to {{.RoomTitle}}! You have permission to become a member of this room because someone has shared this invite with you." InviteFacadeInstruct = "To claim the invite, press the button below which will open a compatible SSB app, if it's installed." InviteFacadeJoin = "Join this room" InviteFacadeInstructQR = "If your SSB app is on another device, you can scan the following QR code to claim your invite on that device:" diff --git a/web/i18n/helper.go b/web/i18n/helper.go index 00c9187..d3dd1fb 100644 --- a/web/i18n/helper.go +++ b/web/i18n/helper.go @@ -245,6 +245,11 @@ func (h Helper) GetRenderFuncs() []render.Option { loc := h.FromRequest(r) return loc.LocalizeSimple }), + + render.InjectTemplateFunc("i18nWithData", func(r *http.Request) interface{} { + loc := h.FromRequest(r) + return loc.LocalizeWithData + }), } return opts } @@ -260,7 +265,19 @@ func (l Localizer) LocalizeSimple(messageID string) template.HTML { panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err)) } -func (l Localizer) LocalizeWithData(messageID string, tplData map[string]string) template.HTML { +func (l Localizer) LocalizeWithData(messageID string, labelsAndData ...string) template.HTML { + n := len(labelsAndData) + if n%2 != 0 { + panic(fmt.Errorf("expected an even amount of labels and data. got %d", n)) + } + + tplData := make(map[string]string, n/2) + for i := 0; i < n; i += 2 { + key := labelsAndData[i] + data := labelsAndData[i+1] + tplData[key] = data + } + msg, err := l.loc.Localize(&i18n.LocalizeConfig{ MessageID: messageID, TemplateData: tplData, diff --git a/web/router/complete.go b/web/router/complete.go index a85ec53..382ca9a 100644 --- a/web/router/complete.go +++ b/web/router/complete.go @@ -24,6 +24,8 @@ const ( MembersChangePasswordForm = "members:change-password:form" MembersChangePassword = "members:change-password" + + OpenModeCreateInvite = "open:invites:create" ) // CompleteApp constructs a mux.Router containing the routes for batch Complete html frontend @@ -40,6 +42,7 @@ func CompleteApp() *mux.Router { m.Path("/members/change-password").Methods("GET").Name(MembersChangePasswordForm) m.Path("/members/change-password").Methods("POST").Name(MembersChangePassword) + m.Path("/create-invite").Methods("GET").Name(OpenModeCreateInvite) m.Path("/join").Methods("GET").Name(CompleteInviteFacade) m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback) m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID) diff --git a/web/templates/base.tmpl b/web/templates/base.tmpl index d01813c..91978c5 100644 --- a/web/templates/base.tmpl +++ b/web/templates/base.tmpl @@ -46,10 +46,18 @@ >{{i18n "AuthSignOut"}} {{else}} + + {{if privacy_mode_is "ModeOpen"}} + {{i18n "AdminInvitesCreate"}} + {{end}} {{i18n "AuthSignIn"}} + {{end}} diff --git a/web/templates/invite/facade.tmpl b/web/templates/invite/facade.tmpl index f3469a7..2cd13c8 100644 --- a/web/templates/invite/facade.tmpl +++ b/web/templates/invite/facade.tmpl @@ -1,7 +1,7 @@ {{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }} {{ define "content" }}
-

Welcome to {{.RoomTitle}}! {{i18n "InviteFacadeWelcome"}}

+

{{i18nWithData "InviteFacadeWelcome" "RoomTitle" .RoomTitle}}

{{i18n "InviteFacadeInstruct"}}