mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-11-25 05:40:24 +00:00
[feature] New user sign-up via web page (#2796)
* [feature] User sign-up form and admin notifs * add chosen + filtered languages to migration * remove stray comment * chosen languages schmosen schmanguages * proper error on local account missing
This commit is contained in:
parent
a483bd9e38
commit
9fb8a78f91
68 changed files with 1456 additions and 437 deletions
|
@ -186,9 +186,13 @@ var Confirm action.GTSAction = func(ctx context.Context) error {
|
|||
user.Approved = func() *bool { a := true; return &a }()
|
||||
user.Email = user.UnconfirmedEmail
|
||||
user.ConfirmedAt = time.Now()
|
||||
user.SignUpIP = nil
|
||||
return state.DB.UpdateUser(
|
||||
ctx, user,
|
||||
"approved", "email", "confirmed_at",
|
||||
"approved",
|
||||
"email",
|
||||
"confirmed_at",
|
||||
"sign_up_ip",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -100,6 +100,10 @@ var Start action.GTSAction = func(ctx context.Context) error {
|
|||
return fmt.Errorf("error creating instance instance: %s", err)
|
||||
}
|
||||
|
||||
if err := dbService.CreateInstanceApplication(ctx); err != nil {
|
||||
return fmt.Errorf("error creating instance application: %s", err)
|
||||
}
|
||||
|
||||
// Get the instance account
|
||||
// (we'll need this later).
|
||||
instanceAccount, err := dbService.GetInstanceAccount(ctx, "")
|
||||
|
|
|
@ -439,8 +439,8 @@ definitions:
|
|||
x-go-name: ID
|
||||
invite_request:
|
||||
description: |-
|
||||
The reason given when requesting an invite.
|
||||
Null if not known / remote account.
|
||||
The reason given when signing up.
|
||||
Null if no reason / remote account.
|
||||
example: Pleaaaaaaaaaaaaaaase!!
|
||||
type: string
|
||||
x-go-name: InviteRequest
|
||||
|
@ -1842,13 +1842,14 @@ definitions:
|
|||
type:
|
||||
description: |-
|
||||
The type of event that resulted in the notification.
|
||||
follow = Someone followed you
|
||||
follow_request = Someone requested to follow you
|
||||
mention = Someone mentioned you in their status
|
||||
reblog = Someone boosted one of your statuses
|
||||
favourite = Someone favourited one of your statuses
|
||||
poll = A poll you have voted in or created has ended
|
||||
status = Someone you enabled notifications for has posted a status
|
||||
follow = Someone followed you. `account` will be set.
|
||||
follow_request = Someone requested to follow you. `account` will be set.
|
||||
mention = Someone mentioned you in their status. `status` will be set. `account` will be set.
|
||||
reblog = Someone boosted one of your statuses. `status` will be set. `account` will be set.
|
||||
favourite = Someone favourited one of your statuses. `status` will be set. `account` will be set.
|
||||
poll = A poll you have voted in or created has ended. `status` will be set. `account` will be set.
|
||||
status = Someone you enabled notifications for has posted a status. `status` will be set. `account` will be set.
|
||||
admin.sign_up = Someone has signed up for a new account on the instance. `account` will be set.
|
||||
type: string
|
||||
x-go-name: Type
|
||||
title: Notification represents a notification of an event relevant to the user.
|
||||
|
@ -2773,6 +2774,8 @@ paths:
|
|||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"422":
|
||||
description: Unprocessable. Your account creation request cannot be processed because either too many accounts have been created on this instance in the last 24h, or the pending account backlog is full.
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
|
|
|
@ -14,11 +14,6 @@
|
|||
# Default: true
|
||||
accounts-registration-open: true
|
||||
|
||||
# Bool. Do sign up requests require approval from an admin/moderator before an account can sign in/use the server?
|
||||
# Options: [true, false]
|
||||
# Default: true
|
||||
accounts-approval-required: true
|
||||
|
||||
# Bool. Are sign up requests required to submit a reason for the request (eg., an explanation of why they want to join the instance)?
|
||||
# Options: [true, false]
|
||||
# Default: true
|
||||
|
|
|
@ -411,11 +411,6 @@ instance-inject-mastodon-version: false
|
|||
# Default: true
|
||||
accounts-registration-open: true
|
||||
|
||||
# Bool. Do sign up requests require approval from an admin/moderator before an account can sign in/use the server?
|
||||
# Options: [true, false]
|
||||
# Default: true
|
||||
accounts-approval-required: true
|
||||
|
||||
# Bool. Are sign up requests required to submit a reason for the request (eg., an explanation of why they want to join the instance)?
|
||||
# Options: [true, false]
|
||||
# Default: true
|
||||
|
|
|
@ -25,7 +25,6 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
|
@ -67,6 +66,11 @@ import (
|
|||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '422':
|
||||
// description: >-
|
||||
// Unprocessable. Your account creation request cannot be processed
|
||||
// because either too many accounts have been created on this instance
|
||||
// in the last 24h, or the pending account backlog is full.
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||
|
@ -87,7 +91,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeCreateAccount(form); err != nil {
|
||||
if err := validate.CreateAccount(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
@ -101,7 +105,25 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
|||
}
|
||||
form.IP = signUpIP
|
||||
|
||||
ti, errWithCode := m.processor.Account().Create(c.Request.Context(), authed.Token, authed.Application, form)
|
||||
// Create the new account + user.
|
||||
ctx := c.Request.Context()
|
||||
user, errWithCode := m.processor.Account().Create(
|
||||
ctx,
|
||||
authed.Application,
|
||||
form,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Get a token for the new user.
|
||||
ti, errWithCode := m.processor.Account().TokenForNewUser(
|
||||
ctx,
|
||||
authed.Token,
|
||||
authed.Application,
|
||||
user,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
|
@ -109,40 +131,3 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
|||
|
||||
apiutil.JSON(c, http.StatusOK, ti)
|
||||
}
|
||||
|
||||
// validateNormalizeCreateAccount checks through all the necessary prerequisites for creating a new account,
|
||||
// according to the provided account create request. If the account isn't eligible, an error will be returned.
|
||||
// Side effect: normalizes the provided language tag for the user's locale.
|
||||
func validateNormalizeCreateAccount(form *apimodel.AccountCreateRequest) error {
|
||||
if form == nil {
|
||||
return errors.New("form was nil")
|
||||
}
|
||||
|
||||
if !config.GetAccountsRegistrationOpen() {
|
||||
return errors.New("registration is not open for this server")
|
||||
}
|
||||
|
||||
if err := validate.Username(form.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validate.Email(form.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validate.Password(form.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !form.Agreement {
|
||||
return errors.New("agreement to terms and conditions not given")
|
||||
}
|
||||
|
||||
locale, err := validate.Language(form.Locale)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form.Locale = locale
|
||||
|
||||
return validate.SignUpReason(form.Reason, config.GetAccountsReasonRequired())
|
||||
}
|
||||
|
|
|
@ -192,7 +192,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": "118.44.18.196",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -249,7 +249,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": "89.122.255.1",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -295,7 +295,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": "89.122.255.1",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -354,7 +354,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": "118.44.18.196",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -576,7 +576,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": "118.44.18.196",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -798,7 +798,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": "118.44.18.196",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
|
|
@ -50,8 +50,8 @@ type AdminAccountInfo struct {
|
|||
// The locale of the account. (ISO 639 Part 1 two-letter language code)
|
||||
// example: en
|
||||
Locale string `json:"locale"`
|
||||
// The reason given when requesting an invite.
|
||||
// Null if not known / remote account.
|
||||
// The reason given when signing up.
|
||||
// Null if no reason / remote account.
|
||||
// example: Pleaaaaaaaaaaaaaaase!!
|
||||
InviteRequest *string `json:"invite_request"`
|
||||
// The current role of the account.
|
||||
|
|
|
@ -26,13 +26,14 @@ type Notification struct {
|
|||
// The id of the notification in the database.
|
||||
ID string `json:"id"`
|
||||
// The type of event that resulted in the notification.
|
||||
// follow = Someone followed you
|
||||
// follow_request = Someone requested to follow you
|
||||
// mention = Someone mentioned you in their status
|
||||
// reblog = Someone boosted one of your statuses
|
||||
// favourite = Someone favourited one of your statuses
|
||||
// poll = A poll you have voted in or created has ended
|
||||
// status = Someone you enabled notifications for has posted a status
|
||||
// follow = Someone followed you. `account` will be set.
|
||||
// follow_request = Someone requested to follow you. `account` will be set.
|
||||
// mention = Someone mentioned you in their status. `status` will be set. `account` will be set.
|
||||
// reblog = Someone boosted one of your statuses. `status` will be set. `account` will be set.
|
||||
// favourite = Someone favourited one of your statuses. `status` will be set. `account` will be set.
|
||||
// poll = A poll you have voted in or created has ended. `status` will be set. `account` will be set.
|
||||
// status = Someone you enabled notifications for has posted a status. `status` will be set. `account` will be set.
|
||||
// admin.sign_up = Someone has signed up for a new account on the instance. `account` will be set.
|
||||
Type string `json:"type"`
|
||||
// The timestamp of the notification (ISO 8601 Datetime)
|
||||
CreatedAt string `json:"created_at"`
|
||||
|
|
14
internal/cache/size.go
vendored
14
internal/cache/size.go
vendored
|
@ -252,7 +252,6 @@ func sizeofAccountSettings() uintptr {
|
|||
AccountID: exampleID,
|
||||
CreatedAt: exampleTime,
|
||||
UpdatedAt: exampleTime,
|
||||
Reason: exampleText,
|
||||
Privacy: gtsmodel.VisibilityFollowersOnly,
|
||||
Sensitive: util.Ptr(true),
|
||||
Language: "fr",
|
||||
|
@ -629,11 +628,8 @@ func sizeofUser() uintptr {
|
|||
Email: exampleURI,
|
||||
AccountID: exampleID,
|
||||
EncryptedPassword: exampleTextSmall,
|
||||
CurrentSignInAt: exampleTime,
|
||||
LastSignInAt: exampleTime,
|
||||
InviteID: exampleID,
|
||||
ChosenLanguages: []string{"en", "fr", "jp"},
|
||||
FilteredLanguages: []string{"en", "fr", "jp"},
|
||||
Reason: exampleText,
|
||||
Locale: "en",
|
||||
CreatedByApplicationID: exampleID,
|
||||
LastEmailedAt: exampleTime,
|
||||
|
@ -641,10 +637,10 @@ func sizeofUser() uintptr {
|
|||
ConfirmationSentAt: exampleTime,
|
||||
ConfirmedAt: exampleTime,
|
||||
UnconfirmedEmail: exampleURI,
|
||||
Moderator: func() *bool { ok := true; return &ok }(),
|
||||
Admin: func() *bool { ok := true; return &ok }(),
|
||||
Disabled: func() *bool { ok := true; return &ok }(),
|
||||
Approved: func() *bool { ok := true; return &ok }(),
|
||||
Moderator: util.Ptr(false),
|
||||
Admin: util.Ptr(false),
|
||||
Disabled: util.Ptr(false),
|
||||
Approved: util.Ptr(false),
|
||||
ResetPasswordToken: exampleTextSmall,
|
||||
ResetPasswordSentAt: exampleTime,
|
||||
ExternalID: exampleID,
|
||||
|
|
|
@ -88,7 +88,6 @@ type Configuration struct {
|
|||
InstanceLanguages language.Languages `name:"instance-languages" usage:"BCP47 language tags for the instance. Used to indicate the preferred languages of instance residents (in order from most-preferred to least-preferred)."`
|
||||
|
||||
AccountsRegistrationOpen bool `name:"accounts-registration-open" usage:"Allow anyone to submit an account signup request. If false, server will be invite-only."`
|
||||
AccountsApprovalRequired bool `name:"accounts-approval-required" usage:"Do account signups require approval by an admin or moderator before user can log in? If false, new registrations will be automatically approved."`
|
||||
AccountsReasonRequired bool `name:"accounts-reason-required" usage:"Do new account signups require a reason to be submitted on registration?"`
|
||||
AccountsAllowCustomCSS bool `name:"accounts-allow-custom-css" usage:"Allow accounts to enable custom CSS for their profile pages and statuses."`
|
||||
AccountsCustomCSSLength int `name:"accounts-custom-css-length" usage:"Maximum permitted length (characters) of custom CSS for accounts."`
|
||||
|
|
|
@ -67,7 +67,6 @@ var Defaults = Configuration{
|
|||
InstanceLanguages: make(language.Languages, 0),
|
||||
|
||||
AccountsRegistrationOpen: true,
|
||||
AccountsApprovalRequired: true,
|
||||
AccountsReasonRequired: true,
|
||||
AccountsAllowCustomCSS: false,
|
||||
AccountsCustomCSSLength: 10000,
|
||||
|
|
|
@ -93,7 +93,6 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
|
|||
|
||||
// Accounts
|
||||
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
|
||||
cmd.Flags().Bool(AccountsApprovalRequiredFlag(), cfg.AccountsApprovalRequired, fieldtag("AccountsApprovalRequired", "usage"))
|
||||
cmd.Flags().Bool(AccountsReasonRequiredFlag(), cfg.AccountsReasonRequired, fieldtag("AccountsReasonRequired", "usage"))
|
||||
cmd.Flags().Bool(AccountsAllowCustomCSSFlag(), cfg.AccountsAllowCustomCSS, fieldtag("AccountsAllowCustomCSS", "usage"))
|
||||
|
||||
|
|
|
@ -1000,31 +1000,6 @@ func GetAccountsRegistrationOpen() bool { return global.GetAccountsRegistrationO
|
|||
// SetAccountsRegistrationOpen safely sets the value for global configuration 'AccountsRegistrationOpen' field
|
||||
func SetAccountsRegistrationOpen(v bool) { global.SetAccountsRegistrationOpen(v) }
|
||||
|
||||
// GetAccountsApprovalRequired safely fetches the Configuration value for state's 'AccountsApprovalRequired' field
|
||||
func (st *ConfigState) GetAccountsApprovalRequired() (v bool) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.AccountsApprovalRequired
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetAccountsApprovalRequired safely sets the Configuration value for state's 'AccountsApprovalRequired' field
|
||||
func (st *ConfigState) SetAccountsApprovalRequired(v bool) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.AccountsApprovalRequired = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// AccountsApprovalRequiredFlag returns the flag name for the 'AccountsApprovalRequired' field
|
||||
func AccountsApprovalRequiredFlag() string { return "accounts-approval-required" }
|
||||
|
||||
// GetAccountsApprovalRequired safely fetches the value for global configuration 'AccountsApprovalRequired' field
|
||||
func GetAccountsApprovalRequired() bool { return global.GetAccountsApprovalRequired() }
|
||||
|
||||
// SetAccountsApprovalRequired safely sets the value for global configuration 'AccountsApprovalRequired' field
|
||||
func SetAccountsApprovalRequired(v bool) { global.SetAccountsApprovalRequired(v) }
|
||||
|
||||
// GetAccountsReasonRequired safely fetches the Configuration value for state's 'AccountsReasonRequired' field
|
||||
func (st *ConfigState) GetAccountsReasonRequired() (v bool) {
|
||||
st.mutex.RLock()
|
||||
|
|
|
@ -29,6 +29,9 @@ type Account interface {
|
|||
// GetAccountByID returns one account with the given ID, or an error if something goes wrong.
|
||||
GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountsByIDs returns accounts corresponding to given IDs.
|
||||
GetAccountsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.Account, error)
|
||||
|
||||
// GetAccountByURI returns one account with the given URI, or an error if something goes wrong.
|
||||
GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ package db
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
@ -36,7 +37,7 @@ type Admin interface {
|
|||
// C) something went wrong in the db
|
||||
IsEmailAvailable(ctx context.Context, email string) (bool, error)
|
||||
|
||||
// NewSignup creates a new user in the database with the given parameters.
|
||||
// NewSignup creates a new user + account in the database with the given parameters.
|
||||
// By the time this function is called, it should be assumed that all the parameters have passed validation!
|
||||
NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (*gtsmodel.User, error)
|
||||
|
||||
|
@ -50,6 +51,23 @@ type Admin interface {
|
|||
// This is needed for things like serving instance information through /api/v1/instance
|
||||
CreateInstanceInstance(ctx context.Context) error
|
||||
|
||||
// CreateInstanceApplication creates an application in the database
|
||||
// for use in processing signups etc through the sign-up form.
|
||||
CreateInstanceApplication(ctx context.Context) error
|
||||
|
||||
// GetInstanceApplication gets the instance application
|
||||
// (ie., the application owned by the instance account).
|
||||
GetInstanceApplication(ctx context.Context) (*gtsmodel.Application, error)
|
||||
|
||||
// CountApprovedSignupsSince counts the number of new account
|
||||
// sign-ups approved on this instance since the given time.
|
||||
CountApprovedSignupsSince(ctx context.Context, since time.Time) (int, error)
|
||||
|
||||
// CountUnhandledSignups counts the number of account sign-ups
|
||||
// that have not yet been approved or denied. In other words,
|
||||
// the number of pending sign-ups sitting in the backlog.
|
||||
CountUnhandledSignups(ctx context.Context) (int, error)
|
||||
|
||||
/*
|
||||
ACTION FUNCS
|
||||
*/
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
|
@ -121,7 +122,6 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
|
|||
|
||||
settings := >smodel.AccountSettings{
|
||||
AccountID: accountID,
|
||||
Reason: newSignup.Reason,
|
||||
Privacy: gtsmodel.VisibilityDefault,
|
||||
}
|
||||
|
||||
|
@ -197,6 +197,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
|
|||
Account: account,
|
||||
EncryptedPassword: string(encryptedPassword),
|
||||
SignUpIP: newSignup.SignUpIP.To4(),
|
||||
Reason: newSignup.Reason,
|
||||
Locale: newSignup.Locale,
|
||||
UnconfirmedEmail: newSignup.Email,
|
||||
CreatedByApplicationID: newSignup.AppID,
|
||||
|
@ -331,6 +332,113 @@ func (a *adminDB) CreateInstanceInstance(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *adminDB) CreateInstanceApplication(ctx context.Context) error {
|
||||
// Check if instance application already exists.
|
||||
// Instance application client_id always = the
|
||||
// instance account's ID so this is an easy check.
|
||||
instanceAcct, err := a.state.DB.GetInstanceAccount(ctx, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exists, err := exists(
|
||||
ctx,
|
||||
a.db.
|
||||
NewSelect().
|
||||
Column("application.id").
|
||||
TableExpr("? AS ?", bun.Ident("applications"), bun.Ident("application")).
|
||||
Where("? = ?", bun.Ident("application.client_id"), instanceAcct.ID),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
log.Infof(ctx, "instance application already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate new IDs for this
|
||||
// application and its client.
|
||||
protocol := config.GetProtocol()
|
||||
host := config.GetHost()
|
||||
url := protocol + "://" + host
|
||||
|
||||
clientID := instanceAcct.ID
|
||||
clientSecret := uuid.NewString()
|
||||
appID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate the application
|
||||
// to put in the database.
|
||||
app := >smodel.Application{
|
||||
ID: appID,
|
||||
Name: host + " instance application",
|
||||
Website: url,
|
||||
RedirectURI: url,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
Scopes: "write:accounts",
|
||||
}
|
||||
|
||||
// Store it.
|
||||
if err := a.state.DB.PutApplication(ctx, app); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Model an oauth client
|
||||
// from the application.
|
||||
oc := >smodel.Client{
|
||||
ID: clientID,
|
||||
Secret: clientSecret,
|
||||
Domain: url,
|
||||
}
|
||||
|
||||
// Store it.
|
||||
return a.state.DB.Put(ctx, oc)
|
||||
}
|
||||
|
||||
func (a *adminDB) GetInstanceApplication(ctx context.Context) (*gtsmodel.Application, error) {
|
||||
// Instance app clientID == instanceAcct.ID,
|
||||
// so get the instance account first.
|
||||
instanceAcct, err := a.state.DB.GetInstanceAccount(ctx, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
app := new(gtsmodel.Application)
|
||||
if err := a.db.
|
||||
NewSelect().
|
||||
Model(app).
|
||||
Where("? = ?", bun.Ident("application.client_id"), instanceAcct.ID).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return app, nil
|
||||
}
|
||||
|
||||
func (a *adminDB) CountApprovedSignupsSince(ctx context.Context, since time.Time) (int, error) {
|
||||
return a.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")).
|
||||
Where("? > ?", bun.Ident("user.created_at"), since).
|
||||
Where("? = ?", bun.Ident("user.approved"), true).
|
||||
Count(ctx)
|
||||
}
|
||||
|
||||
func (a *adminDB) CountUnhandledSignups(ctx context.Context) (int, error) {
|
||||
return a.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")).
|
||||
// Approved is false by default.
|
||||
// Explicitly rejected sign-ups end up elsewhere.
|
||||
Where("? = ?", bun.Ident("user.approved"), false).
|
||||
Count(ctx)
|
||||
}
|
||||
|
||||
/*
|
||||
ACTION FUNCS
|
||||
*/
|
||||
|
|
|
@ -380,3 +380,33 @@ func (i *instanceDB) GetInstanceModeratorAddresses(ctx context.Context) ([]strin
|
|||
|
||||
return addresses, nil
|
||||
}
|
||||
|
||||
func (i *instanceDB) GetInstanceModerators(ctx context.Context) ([]*gtsmodel.Account, error) {
|
||||
accountIDs := []string{}
|
||||
|
||||
// Select account IDs of approved, confirmed,
|
||||
// and enabled moderators or admins.
|
||||
|
||||
q := i.db.
|
||||
NewSelect().
|
||||
TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")).
|
||||
Column("user.account_id").
|
||||
Where("? = ?", bun.Ident("user.approved"), true).
|
||||
Where("? IS NOT NULL", bun.Ident("user.confirmed_at")).
|
||||
Where("? = ?", bun.Ident("user.disabled"), false).
|
||||
WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery {
|
||||
return q.
|
||||
Where("? = ?", bun.Ident("user.moderator"), true).
|
||||
WhereOr("? = ?", bun.Ident("user.admin"), true)
|
||||
})
|
||||
|
||||
if err := q.Scan(ctx, &accountIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(accountIDs) == 0 {
|
||||
return nil, db.ErrNoEntries
|
||||
}
|
||||
|
||||
return i.state.DB.GetAccountsByIDs(ctx, accountIDs)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import (
|
|||
"context"
|
||||
|
||||
oldgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230328203024_migration_fix"
|
||||
newgtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
newgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20240318115336_account_settings"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
type Visibility string
|
||||
|
||||
// AccountSettings models settings / preferences for a local, non-instance account.
|
||||
type AccountSettings struct {
|
||||
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated.
|
||||
Reason string `bun:",nullzero"` // What reason was given for signing up when this account was created?
|
||||
Privacy Visibility `bun:",nullzero"` // Default post privacy for this account
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default?
|
||||
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
|
||||
StatusContentType string `bun:",nullzero"` // What is the default format for statuses posted by this account (only for local accounts).
|
||||
Theme string `bun:",nullzero"` // Preset CSS theme filename selected by this Account (empty string if nothing set).
|
||||
CustomCSS string `bun:",nullzero"` // Custom CSS that should be displayed for this Account's profile and statuses.
|
||||
EnableRSS *bool `bun:",nullzero,notnull,default:false"` // enable RSS feed subscription for this account's public posts at [URL]/feed
|
||||
HideCollections *bool `bun:",nullzero,notnull,default:false"` // Hide this account's followers/following collections.
|
||||
}
|
124
internal/db/bundb/migrations/20240401130338_sign_up.go
Normal file
124
internal/db/bundb/migrations/20240401130338_sign_up.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
// Add reason to users table.
|
||||
_, err := db.ExecContext(ctx,
|
||||
"ALTER TABLE ? ADD COLUMN ? TEXT",
|
||||
bun.Ident("users"), bun.Ident("reason"),
|
||||
)
|
||||
if err != nil {
|
||||
e := err.Error()
|
||||
if !(strings.Contains(e, "already exists") ||
|
||||
strings.Contains(e, "duplicate column name") ||
|
||||
strings.Contains(e, "SQLSTATE 42701")) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
// Get reasons from
|
||||
// account settings.
|
||||
type idReason struct {
|
||||
AccountID string
|
||||
Reason string
|
||||
}
|
||||
|
||||
reasons := []idReason{}
|
||||
if err := tx.
|
||||
NewSelect().
|
||||
Table("account_settings").
|
||||
Column("account_id", "reason").
|
||||
Scan(ctx, &reasons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add each reason to appropriate user.
|
||||
for _, r := range reasons {
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Table("users").
|
||||
Set("? = ?", bun.Ident("reason"), r.Reason).
|
||||
Where("? = ?", bun.Ident("account_id"), r.AccountID).
|
||||
Exec(ctx, &reasons); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Remove now-unused column
|
||||
// from account settings.
|
||||
if _, err := tx.
|
||||
NewDropColumn().
|
||||
Table("account_settings").
|
||||
Column("reason").
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove now-unused columns from users.
|
||||
for _, column := range []string{
|
||||
"current_sign_in_at",
|
||||
"current_sign_in_ip",
|
||||
"last_sign_in_at",
|
||||
"last_sign_in_ip",
|
||||
"sign_in_count",
|
||||
"chosen_languages",
|
||||
"filtered_languages",
|
||||
} {
|
||||
if _, err := tx.
|
||||
NewDropColumn().
|
||||
Table("users").
|
||||
Column(column).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create new UsersDenied table.
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model(>smodel.DeniedUser{}).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -176,7 +176,7 @@ func (suite *NotificationTestSuite) TestDeleteNotificationsOriginatingFromAndTar
|
|||
}
|
||||
|
||||
for _, n := range notif {
|
||||
if n.OriginAccountID == originAccount.ID || n.TargetAccountID == targetAccount.ID {
|
||||
if n.OriginAccountID == originAccount.ID && n.TargetAccountID == targetAccount.ID {
|
||||
suite.FailNowf(
|
||||
"",
|
||||
"no notifications with origin account id %s and target account %s should remain",
|
||||
|
|
|
@ -58,4 +58,8 @@ type Instance interface {
|
|||
// GetInstanceModeratorAddresses returns a slice of email addresses belonging to active
|
||||
// (as in, not suspended) moderators + admins on this instance.
|
||||
GetInstanceModeratorAddresses(ctx context.Context) ([]string, error)
|
||||
|
||||
// GetInstanceModerators returns a slice of accounts belonging to active
|
||||
// (as in, non suspended) moderators + admins on this instance.
|
||||
GetInstanceModerators(ctx context.Context) ([]*gtsmodel.Account, error)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ func (suite *EmailTestSuite) TestTemplateConfirm() {
|
|||
|
||||
suite.sender.SendConfirmEmail("user@example.org", confirmData)
|
||||
suite.Len(suite.sentEmails, 1)
|
||||
suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an account on https://example.org.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org\r\n\r\n", suite.sentEmails["user@example.org"])
|
||||
suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an account on https://example.org.\r\n\r\nTo use your account, you must confirm that this is your email address.\r\n\r\nTo confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org.\r\n\r\n", suite.sentEmails["user@example.org"])
|
||||
}
|
||||
|
||||
func (suite *EmailTestSuite) TestTemplateReset() {
|
||||
|
|
|
@ -68,6 +68,10 @@ func (s *noopSender) SendReportClosedEmail(toAddress string, data ReportClosedDa
|
|||
return s.sendTemplate(reportClosedTemplate, reportClosedSubject, data, toAddress)
|
||||
}
|
||||
|
||||
func (s *noopSender) SendNewSignupEmail(toAddresses []string, data NewSignupData) error {
|
||||
return s.sendTemplate(newSignupTemplate, newSignupSubject, data, toAddresses...)
|
||||
}
|
||||
|
||||
func (s *noopSender) sendTemplate(template string, subject string, data any, toAddresses ...string) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, template, data); err != nil {
|
||||
|
|
|
@ -46,6 +46,13 @@ type Sender interface {
|
|||
// SendReportClosedEmail sends an email notification to the given address, letting them
|
||||
// know that a report that they created has been closed / resolved by an admin.
|
||||
SendReportClosedEmail(toAddress string, data ReportClosedData) error
|
||||
|
||||
// SendNewSignupEmail sends an email notification to the given addresses,
|
||||
// letting them know that a new sign-up has been submitted to the instance.
|
||||
//
|
||||
// It is expected that the toAddresses have already been filtered to ensure
|
||||
// that they all belong to active admins + moderators.
|
||||
SendNewSignupEmail(toAddress []string, data NewSignupData) error
|
||||
}
|
||||
|
||||
// NewSender returns a new email Sender interface with the given configuration, or an error if something goes wrong.
|
||||
|
|
42
internal/email/signup.go
Normal file
42
internal/email/signup.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package email
|
||||
|
||||
var (
|
||||
newSignupTemplate = "email_new_signup.tmpl"
|
||||
newSignupSubject = "GoToSocial New Sign-Up"
|
||||
)
|
||||
|
||||
type NewSignupData struct {
|
||||
// URL of the instance to present to the receiver.
|
||||
InstanceURL string
|
||||
// Name of the instance to present to the receiver.
|
||||
InstanceName string
|
||||
// Email address sign-up was created with.
|
||||
SignupEmail string
|
||||
// Username submitted on the sign-up form.
|
||||
SignupUsername string
|
||||
// Reason given on the sign-up form.
|
||||
SignupReason string
|
||||
// URL to open the sign-up in the settings panel.
|
||||
SignupURL string
|
||||
}
|
||||
|
||||
func (s *sender) SendNewSignupEmail(toAddresses []string, data NewSignupData) error {
|
||||
return s.sendTemplate(newSignupTemplate, newSignupSubject, data, toAddresses...)
|
||||
}
|
|
@ -24,7 +24,6 @@ type AccountSettings struct {
|
|||
AccountID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // AccountID that owns this settings.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created.
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item was last updated.
|
||||
Reason string `bun:",nullzero"` // What reason was given for signing up when this account was created?
|
||||
Privacy Visibility `bun:",nullzero"` // Default post privacy for this account
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Set posts from this account to sensitive by default?
|
||||
Language string `bun:",nullzero,notnull,default:'en'"` // What language does this account post in?
|
||||
|
|
|
@ -46,4 +46,5 @@ const (
|
|||
NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses
|
||||
NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended
|
||||
NotificationStatus NotificationType = "status" // NotificationStatus -- someone you enabled notifications for has posted a status.
|
||||
NotificationSignup NotificationType = "admin.sign_up" // NotificationSignup -- someone has submitted a new account sign-up to the instance.
|
||||
)
|
||||
|
|
|
@ -22,8 +22,14 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account.
|
||||
// To cross reference this local user with their account (which can be local or remote), use the AccountID field.
|
||||
// User represents one signed-up user of this GoToSocial instance.
|
||||
//
|
||||
// User may not necessarily be approved yet; in other words, this
|
||||
// model is used for both active users and signed-up but not yet
|
||||
// approved users.
|
||||
//
|
||||
// Sign-ups that have been denied rather than
|
||||
// approved are stored as DeniedUser instead.
|
||||
type User struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
|
@ -32,15 +38,9 @@ type User struct {
|
|||
AccountID string `bun:"type:CHAR(26),nullzero,notnull,unique"` // The id of the local gtsmodel.Account entry for this user.
|
||||
Account *Account `bun:"rel:belongs-to"` // Pointer to the account of this user that corresponds to AccountID.
|
||||
EncryptedPassword string `bun:",nullzero,notnull"` // The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables.
|
||||
SignUpIP net.IP `bun:",nullzero"` // From what IP was this user created?
|
||||
CurrentSignInAt time.Time `bun:"type:timestamptz,nullzero"` // When did the user sign in with their current session.
|
||||
CurrentSignInIP net.IP `bun:",nullzero"` // What's the most recent IP of this user
|
||||
LastSignInAt time.Time `bun:"type:timestamptz,nullzero"` // When did this user last sign in?
|
||||
LastSignInIP net.IP `bun:",nullzero"` // What's the previous IP of this user?
|
||||
SignInCount int `bun:",notnull,default:0"` // How many times has this user signed in?
|
||||
SignUpIP net.IP `bun:",nullzero"` // IP this user used to sign up. Only stored for pending sign-ups.
|
||||
InviteID string `bun:"type:CHAR(26),nullzero"` // id of the user who invited this user (who let this joker in?)
|
||||
ChosenLanguages []string `bun:",nullzero"` // What languages does this user want to see?
|
||||
FilteredLanguages []string `bun:",nullzero"` // What languages does this user not want to see?
|
||||
Reason string `bun:",nullzero"` // What reason was given for signing up when this user was created?
|
||||
Locale string `bun:",nullzero"` // In what timezone/locale is this user located?
|
||||
CreatedByApplicationID string `bun:"type:CHAR(26),nullzero"` // Which application id created this user? See gtsmodel.Application
|
||||
CreatedByApplication *Application `bun:"rel:belongs-to"` // Pointer to the application corresponding to createdbyapplicationID.
|
||||
|
@ -58,15 +58,36 @@ type User struct {
|
|||
ExternalID string `bun:",nullzero,unique"` // If the login for the user is managed externally (e.g OIDC), we need to keep a stable reference to the external object (e.g OIDC sub claim)
|
||||
}
|
||||
|
||||
// DeniedUser represents one user sign-up that
|
||||
// was submitted to the instance and denied.
|
||||
type DeniedUser struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||
UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
|
||||
Email string `bun:",nullzero,notnull"` // Email address provided on the sign-up form.
|
||||
Username string `bun:",nullzero,notnull"` // Username provided on the sign-up form.
|
||||
SignUpIP net.IP `bun:",nullzero"` // IP address the sign-up originated from.
|
||||
InviteID string `bun:"type:CHAR(26),nullzero"` // Invite ID provided on the sign-up form (if applicable).
|
||||
Locale string `bun:",nullzero"` // Locale provided on the sign-up form.
|
||||
CreatedByApplicationID string `bun:"type:CHAR(26),nullzero"` // ID of application used to create this sign-up.
|
||||
SignUpReason string `bun:",nullzero"` // Reason provided by user on the sign-up form.
|
||||
PrivateComment string `bun:",nullzero"` // Comment from instance admin about why this sign-up was denied.
|
||||
SendEmail *bool `bun:",nullzero,notnull,default:false"` // Send an email informing user that their sign-up has been denied.
|
||||
Message string `bun:",nullzero"` // Message to include when sending an email to the denied user's email address, if SendEmail is true.
|
||||
}
|
||||
|
||||
// NewSignup models parameters for the creation
|
||||
// of a new user + account on this instance.
|
||||
//
|
||||
// Aside from username, email, and password, it is
|
||||
// fine to use zero values on fields of this struct.
|
||||
//
|
||||
// This struct is not stored in the database,
|
||||
// it's just for passing around parameters.
|
||||
type NewSignup struct {
|
||||
Username string // Username of the new account.
|
||||
Email string // Email address of the user.
|
||||
Password string // Plaintext (not yet hashed) password for the user.
|
||||
Username string // Username of the new account (required).
|
||||
Email string // Email address of the user (required).
|
||||
Password string // Plaintext (not yet hashed) password for the user (required).
|
||||
|
||||
Reason string // Reason given by the user when submitting a sign up request (optional).
|
||||
PreApproved bool // Mark the new user/account as preapproved (optional)
|
||||
|
|
|
@ -20,6 +20,7 @@ package account
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
|
@ -32,15 +33,48 @@ import (
|
|||
)
|
||||
|
||||
// Create processes the given form for creating a new account,
|
||||
// returning an oauth token for that account if successful.
|
||||
// returning a new user (with attached account) if successful.
|
||||
//
|
||||
// Precondition: the form's fields should have already been validated and normalized by the caller.
|
||||
// App should be the app used to create the account.
|
||||
// If nil, the instance app will be used.
|
||||
//
|
||||
// Precondition: the form's fields should have already been
|
||||
// validated and normalized by the caller.
|
||||
func (p *Processor) Create(
|
||||
ctx context.Context,
|
||||
appToken oauth2.TokenInfo,
|
||||
app *gtsmodel.Application,
|
||||
form *apimodel.AccountCreateRequest,
|
||||
) (*apimodel.Token, gtserror.WithCode) {
|
||||
) (*gtsmodel.User, gtserror.WithCode) {
|
||||
const (
|
||||
usersPerDay = 10
|
||||
regBacklog = 20
|
||||
)
|
||||
|
||||
// Ensure no more than usersPerDay
|
||||
// have registered in the last 24h.
|
||||
newUsersCount, err := p.state.DB.CountApprovedSignupsSince(ctx, time.Now().Add(-24*time.Hour))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error counting new users: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if newUsersCount >= usersPerDay {
|
||||
err := fmt.Errorf("this instance has hit its limit of new sign-ups for today; you can try again tomorrow")
|
||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// Ensure the new users backlog isn't full.
|
||||
backlogLen, err := p.state.DB.CountUnhandledSignups(ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error counting registration backlog length: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if backlogLen >= regBacklog {
|
||||
err := fmt.Errorf("this instance's sign-up backlog is currently full; you must wait until pending sign-ups are handled by the admin(s)")
|
||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error checking email availability: %w", err)
|
||||
|
@ -67,38 +101,61 @@ func (p *Processor) Create(
|
|||
reason = form.Reason
|
||||
}
|
||||
|
||||
// Use instance app if no app provided.
|
||||
if app == nil {
|
||||
app, err = p.state.DB.GetInstanceApplication(ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error getting instance app: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := p.state.DB.NewSignup(ctx, gtsmodel.NewSignup{
|
||||
Username: form.Username,
|
||||
Email: form.Email,
|
||||
Password: form.Password,
|
||||
Reason: text.SanitizeToPlaintext(reason),
|
||||
PreApproved: !config.GetAccountsApprovalRequired(), // Mark as approved if no approval required.
|
||||
SignUpIP: form.IP,
|
||||
Locale: form.Locale,
|
||||
AppID: app.ID,
|
||||
Username: form.Username,
|
||||
Email: form.Email,
|
||||
Password: form.Password,
|
||||
Reason: text.SanitizeToPlaintext(reason),
|
||||
SignUpIP: form.IP,
|
||||
Locale: form.Locale,
|
||||
AppID: app.ID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error creating new signup: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Generate access token *before* doing side effects; we
|
||||
// don't want to process side effects if something borks.
|
||||
accessToken, err := p.oauthServer.GenerateUserAccessToken(ctx, appToken, app.ClientSecret, user.ID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error creating new access token for user %s: %w", user.ID, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// There are side effects for creating a new account
|
||||
// (confirmation emails etc), perform these async.
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: user.Account,
|
||||
GTSModel: user,
|
||||
OriginAccount: user.Account,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// TokenForNewUser generates an OAuth Bearer token
|
||||
// for a new user (with account) created by Create().
|
||||
func (p *Processor) TokenForNewUser(
|
||||
ctx context.Context,
|
||||
appToken oauth2.TokenInfo,
|
||||
app *gtsmodel.Application,
|
||||
user *gtsmodel.User,
|
||||
) (*apimodel.Token, gtserror.WithCode) {
|
||||
// Generate access token.
|
||||
accessToken, err := p.oauthServer.GenerateUserAccessToken(
|
||||
ctx,
|
||||
appToken,
|
||||
app.ClientSecret,
|
||||
user.ID,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error creating new access token for user %s: %w", user.ID, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return &apimodel.Token{
|
||||
AccessToken: accessToken.GetAccess(),
|
||||
TokenType: "Bearer",
|
||||
|
|
|
@ -569,11 +569,6 @@ func stubbifyUser(user *gtsmodel.User) ([]string, error) {
|
|||
|
||||
user.EncryptedPassword = string(dummyPassword)
|
||||
user.SignUpIP = net.IPv4zero
|
||||
user.CurrentSignInAt = never
|
||||
user.CurrentSignInIP = net.IPv4zero
|
||||
user.LastSignInAt = never
|
||||
user.LastSignInIP = net.IPv4zero
|
||||
user.SignInCount = 1
|
||||
user.Locale = ""
|
||||
user.CreatedByApplicationID = ""
|
||||
user.LastEmailedAt = never
|
||||
|
@ -585,11 +580,6 @@ func stubbifyUser(user *gtsmodel.User) ([]string, error) {
|
|||
return []string{
|
||||
"encrypted_password",
|
||||
"sign_up_ip",
|
||||
"current_sign_in_at",
|
||||
"current_sign_in_ip",
|
||||
"last_sign_in_at",
|
||||
"last_sign_in_ip",
|
||||
"sign_in_count",
|
||||
"locale",
|
||||
"created_by_application_id",
|
||||
"last_emailed_at",
|
||||
|
|
|
@ -78,11 +78,6 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
|
|||
suite.WithinDuration(time.Now(), updatedUser.UpdatedAt, 1*time.Minute)
|
||||
suite.NotEqual(updatedUser.EncryptedPassword, ogUser.EncryptedPassword)
|
||||
suite.Equal(net.IPv4zero, updatedUser.SignUpIP)
|
||||
suite.Zero(updatedUser.CurrentSignInAt)
|
||||
suite.Equal(net.IPv4zero, updatedUser.CurrentSignInIP)
|
||||
suite.Zero(updatedUser.LastSignInAt)
|
||||
suite.Equal(net.IPv4zero, updatedUser.LastSignInIP)
|
||||
suite.Equal(1, updatedUser.SignInCount)
|
||||
suite.Zero(updatedUser.Locale)
|
||||
suite.Zero(updatedUser.CreatedByApplicationID)
|
||||
suite.Zero(updatedUser.LastEmailedAt)
|
||||
|
|
|
@ -60,31 +60,14 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ma
|
|||
prevMinIDValue = n.ID
|
||||
}
|
||||
|
||||
// Ensure this notification should be shown to requester.
|
||||
if n.OriginAccount != nil {
|
||||
// Account is set, ensure it's visible to notif target.
|
||||
visible, err := p.filter.AccountVisible(ctx, authed.Account, n.OriginAccount)
|
||||
if err != nil {
|
||||
log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %s", n.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !visible {
|
||||
continue
|
||||
}
|
||||
visible, err := p.notifVisible(ctx, n, authed.Account)
|
||||
if err != nil {
|
||||
log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %v", n.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if n.Status != nil {
|
||||
// Status is set, ensure it's visible to notif target.
|
||||
visible, err := p.filter.StatusVisible(ctx, authed.Account, n.Status)
|
||||
if err != nil {
|
||||
log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %s", n.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !visible {
|
||||
continue
|
||||
}
|
||||
if !visible {
|
||||
continue
|
||||
}
|
||||
|
||||
item, err := p.converter.NotificationToAPINotification(ctx, n)
|
||||
|
@ -142,3 +125,44 @@ func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth)
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Processor) notifVisible(
|
||||
ctx context.Context,
|
||||
n *gtsmodel.Notification,
|
||||
acct *gtsmodel.Account,
|
||||
) (bool, error) {
|
||||
// If account is set, ensure it's
|
||||
// visible to notif target.
|
||||
if n.OriginAccount != nil {
|
||||
// If this is a new local account sign-up,
|
||||
// skip normal visibility checking because
|
||||
// origin account won't be confirmed yet.
|
||||
if n.NotificationType == gtsmodel.NotificationSignup {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
visible, err := p.filter.AccountVisible(ctx, acct, n.OriginAccount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !visible {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If status is set, ensure it's
|
||||
// visible to notif target.
|
||||
if n.Status != nil {
|
||||
visible, err := p.filter.StatusVisible(ctx, acct, n.Status)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !visible {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -28,53 +28,78 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
var oneWeek = 168 * time.Hour
|
||||
|
||||
// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link
|
||||
// in a 'confirm your email address' type email.
|
||||
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||
// EmailGetUserForConfirmToken retrieves the user (with account) from
|
||||
// the database for the given "confirm your email" token string.
|
||||
func (p *Processor) EmailGetUserForConfirmToken(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||
if token == "" {
|
||||
return nil, gtserror.NewErrorNotFound(errors.New("no token provided"))
|
||||
err := errors.New("no token provided")
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
user, err := p.state.DB.GetUserByConfirmationToken(ctx, token)
|
||||
if err != nil {
|
||||
if err == db.ErrNoEntries {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
if !errors.Is(err, db.ErrNoEntries) {
|
||||
// Real error.
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
|
||||
// No user found for this token.
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
if user.Account == nil {
|
||||
a, err := p.state.DB.GetAccountByID(ctx, user.AccountID)
|
||||
user.Account, err = p.state.DB.GetAccountByID(ctx, user.AccountID)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
// We need the account for a local user.
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
user.Account = a
|
||||
}
|
||||
|
||||
if !user.Account.SuspendedAt.IsZero() {
|
||||
return nil, gtserror.NewErrorForbidden(fmt.Errorf("ConfirmEmail: account %s is suspended", user.AccountID))
|
||||
err := fmt.Errorf("account %s is suspended", user.AccountID)
|
||||
return nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||
}
|
||||
|
||||
if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
|
||||
// no pending email confirmations so just return OK
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// EmailConfirm processes an email confirmation request,
|
||||
// usually initiated as a result of clicking on a link
|
||||
// in a 'confirm your email address' type email.
|
||||
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||
user, errWithCode := p.EmailGetUserForConfirmToken(ctx, token)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if user.UnconfirmedEmail == "" ||
|
||||
user.UnconfirmedEmail == user.Email {
|
||||
// Confirmed already, just return.
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Ensure token not expired.
|
||||
const oneWeek = 168 * time.Hour
|
||||
if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) {
|
||||
return nil, gtserror.NewErrorForbidden(errors.New("ConfirmEmail: confirmation token expired"))
|
||||
err := errors.New("confirmation token expired (older than one week)")
|
||||
return nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||
}
|
||||
|
||||
// mark the user's email address as confirmed + remove the unconfirmed address and the token
|
||||
updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"}
|
||||
// Mark the user's email address as confirmed,
|
||||
// and remove the unconfirmed address and the token.
|
||||
user.Email = user.UnconfirmedEmail
|
||||
user.UnconfirmedEmail = ""
|
||||
user.ConfirmedAt = time.Now()
|
||||
user.ConfirmationToken = ""
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
if err := p.state.DB.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
|
||||
if err := p.state.DB.UpdateUser(
|
||||
ctx,
|
||||
user,
|
||||
"email",
|
||||
"unconfirmed_email",
|
||||
"confirmed_at",
|
||||
"confirmation_token",
|
||||
); err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() {
|
|||
// confirm with the token set above
|
||||
updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
|
||||
suite.Nil(updatedUser)
|
||||
suite.EqualError(errWithCode, "ConfirmEmail: confirmation token expired")
|
||||
suite.EqualError(errWithCode, "confirmation token expired (older than one week)")
|
||||
}
|
||||
|
||||
func TestEmailConfirmTestSuite(t *testing.T) {
|
||||
|
|
|
@ -209,18 +209,23 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From
|
|||
}
|
||||
|
||||
func (p *clientAPI) CreateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
|
||||
account, ok := cMsg.GTSModel.(*gtsmodel.Account)
|
||||
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Account", cMsg.GTSModel)
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Send a confirmation email to the newly created account.
|
||||
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error getting user for account id %s: %w", account.ID, err)
|
||||
// Notify mods of the new signup.
|
||||
if err := p.surface.notifySignup(ctx, newUser); err != nil {
|
||||
log.Errorf(ctx, "error notifying mods of new sign-up: %v", err)
|
||||
}
|
||||
|
||||
if err := p.surface.emailPleaseConfirm(ctx, user, account.Username); err != nil {
|
||||
// Send "new sign up" email to mods.
|
||||
if err := p.surface.emailAdminNewSignup(ctx, newUser); err != nil {
|
||||
log.Errorf(ctx, "error emailing new signup: %v", err)
|
||||
}
|
||||
|
||||
// Send "please confirm your address" email to the new user.
|
||||
if err := p.surface.emailUserPleaseConfirm(ctx, newUser); err != nil {
|
||||
log.Errorf(ctx, "error emailing confirm: %v", err)
|
||||
}
|
||||
|
||||
|
@ -458,7 +463,7 @@ func (p *clientAPI) UpdateReport(ctx context.Context, cMsg messages.FromClientAP
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := p.surface.emailReportClosed(ctx, report); err != nil {
|
||||
if err := p.surface.emailUserReportClosed(ctx, report); err != nil {
|
||||
log.Errorf(ctx, "error emailing report closed: %v", err)
|
||||
}
|
||||
|
||||
|
@ -644,7 +649,7 @@ func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientA
|
|||
}
|
||||
}
|
||||
|
||||
if err := p.surface.emailReportOpened(ctx, report); err != nil {
|
||||
if err := p.surface.emailAdminReportOpened(ctx, report); err != nil {
|
||||
log.Errorf(ctx, "error emailing report opened: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -473,7 +473,7 @@ func (p *fediAPI) CreateFlag(ctx context.Context, fMsg messages.FromFediAPI) err
|
|||
// TODO: handle additional side effects of flag creation:
|
||||
// - notify admins by dm / notification
|
||||
|
||||
if err := p.surface.emailReportOpened(ctx, incomingReport); err != nil {
|
||||
if err := p.surface.emailAdminReportOpened(ctx, incomingReport); err != nil {
|
||||
log.Errorf(ctx, "error emailing report opened: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -31,41 +31,9 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
)
|
||||
|
||||
func (s *surface) emailReportOpened(ctx context.Context, report *gtsmodel.Report) error {
|
||||
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting instance: %w", err)
|
||||
}
|
||||
|
||||
toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// No registered moderator addresses.
|
||||
return nil
|
||||
}
|
||||
return gtserror.Newf("error getting instance moderator addresses: %w", err)
|
||||
}
|
||||
|
||||
if err := s.state.DB.PopulateReport(ctx, report); err != nil {
|
||||
return gtserror.Newf("error populating report: %w", err)
|
||||
}
|
||||
|
||||
reportData := email.NewReportData{
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
ReportURL: instance.URI + "/settings/admin/reports/" + report.ID,
|
||||
ReportDomain: report.Account.Domain,
|
||||
ReportTargetDomain: report.TargetAccount.Domain,
|
||||
}
|
||||
|
||||
if err := s.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
|
||||
return gtserror.Newf("error emailing instance moderators: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *surface) emailReportClosed(ctx context.Context, report *gtsmodel.Report) error {
|
||||
// emailUserReportClosed emails the user who created the
|
||||
// given report, to inform them the report has been closed.
|
||||
func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Report) error {
|
||||
user, err := s.state.DB.GetUserByAccountID(ctx, report.Account.ID)
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error getting user: %w", err)
|
||||
|
@ -104,7 +72,9 @@ func (s *surface) emailReportClosed(ctx context.Context, report *gtsmodel.Report
|
|||
return s.emailSender.SendReportClosedEmail(user.Email, reportClosedData)
|
||||
}
|
||||
|
||||
func (s *surface) emailPleaseConfirm(ctx context.Context, user *gtsmodel.User, username string) error {
|
||||
// emailUserPleaseConfirm emails the given user
|
||||
// to ask them to confirm their email address.
|
||||
func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error {
|
||||
if user.UnconfirmedEmail == "" ||
|
||||
user.UnconfirmedEmail == user.Email {
|
||||
// User has already confirmed this
|
||||
|
@ -130,7 +100,7 @@ func (s *surface) emailPleaseConfirm(ctx context.Context, user *gtsmodel.User, u
|
|||
if err := s.emailSender.SendConfirmEmail(
|
||||
user.UnconfirmedEmail,
|
||||
email.ConfirmData{
|
||||
Username: username,
|
||||
Username: user.Account.Username,
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
ConfirmLink: confirmLink,
|
||||
|
@ -158,3 +128,77 @@ func (s *surface) emailPleaseConfirm(ctx context.Context, user *gtsmodel.User, u
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// emailAdminReportOpened emails all active moderators/admins
|
||||
// of this instance that a new report has been created.
|
||||
func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.Report) error {
|
||||
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting instance: %w", err)
|
||||
}
|
||||
|
||||
toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// No registered moderator addresses.
|
||||
return nil
|
||||
}
|
||||
return gtserror.Newf("error getting instance moderator addresses: %w", err)
|
||||
}
|
||||
|
||||
if err := s.state.DB.PopulateReport(ctx, report); err != nil {
|
||||
return gtserror.Newf("error populating report: %w", err)
|
||||
}
|
||||
|
||||
reportData := email.NewReportData{
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
ReportURL: instance.URI + "/settings/admin/reports/" + report.ID,
|
||||
ReportDomain: report.Account.Domain,
|
||||
ReportTargetDomain: report.TargetAccount.Domain,
|
||||
}
|
||||
|
||||
if err := s.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
|
||||
return gtserror.Newf("error emailing instance moderators: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// emailAdminNewSignup emails all active moderators/admins of this
|
||||
// instance that a new account sign-up has been submitted to the instance.
|
||||
func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.User) error {
|
||||
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
|
||||
if err != nil {
|
||||
return gtserror.Newf("error getting instance: %w", err)
|
||||
}
|
||||
|
||||
toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// No registered moderator addresses.
|
||||
return nil
|
||||
}
|
||||
return gtserror.Newf("error getting instance moderator addresses: %w", err)
|
||||
}
|
||||
|
||||
// Ensure user populated.
|
||||
if err := s.state.DB.PopulateUser(ctx, newUser); err != nil {
|
||||
return gtserror.Newf("error populating user: %w", err)
|
||||
}
|
||||
|
||||
newSignupData := email.NewSignupData{
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
SignupEmail: newUser.UnconfirmedEmail,
|
||||
SignupUsername: newUser.Account.Username,
|
||||
SignupReason: newUser.Reason,
|
||||
SignupURL: "TODO",
|
||||
}
|
||||
|
||||
if err := s.emailSender.SendNewSignupEmail(toAddresses, newSignupData); err != nil {
|
||||
return gtserror.Newf("error emailing instance moderators: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -333,6 +333,45 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
|
|||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) error {
|
||||
modAccounts, err := s.state.DB.GetInstanceModerators(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
// No registered
|
||||
// mod accounts.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Real error.
|
||||
return gtserror.Newf("error getting instance moderator accounts: %w", err)
|
||||
}
|
||||
|
||||
// Ensure user + account populated.
|
||||
if err := s.state.DB.PopulateUser(ctx, newUser); err != nil {
|
||||
return gtserror.Newf("db error populating new user: %w", err)
|
||||
}
|
||||
|
||||
if err := s.state.DB.PopulateAccount(ctx, newUser.Account); err != nil {
|
||||
return gtserror.Newf("db error populating new user's account: %w", err)
|
||||
}
|
||||
|
||||
// Notify each moderator.
|
||||
var errs gtserror.MultiError
|
||||
for _, mod := range modAccounts {
|
||||
if err := s.notify(ctx,
|
||||
gtsmodel.NotificationSignup,
|
||||
mod,
|
||||
newUser.Account,
|
||||
"",
|
||||
); err != nil {
|
||||
errs.Appendf("error notifying moderator %s: %w", mod.ID, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
// notify creates, inserts, and streams a new
|
||||
// notification to the target account if it
|
||||
// doesn't yet exist with the given parameters.
|
||||
|
@ -342,7 +381,7 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
|
|||
// targets into this function without filtering
|
||||
// for non-local first.
|
||||
//
|
||||
// targetAccountID and originAccountID must be
|
||||
// targetAccount and originAccount must be
|
||||
// set, but statusID can be an empty string.
|
||||
func (s *surface) notify(
|
||||
ctx context.Context,
|
||||
|
|
|
@ -29,11 +29,8 @@ type User struct {
|
|||
Email string `json:"email,omitempty" bun:",nullzero"`
|
||||
AccountID string `json:"accountID" bun:",nullzero"`
|
||||
EncryptedPassword string `json:"encryptedPassword" bun:",nullzero"`
|
||||
CurrentSignInAt *time.Time `json:"currentSignInAt,omitempty" bun:",nullzero"`
|
||||
LastSignInAt *time.Time `json:"lastSignInAt,omitempty" bun:",nullzero"`
|
||||
Reason string `json:"reason" bun:",nullzero"`
|
||||
InviteID string `json:"inviteID,omitempty" bun:",nullzero"`
|
||||
ChosenLanguages []string `json:"chosenLanguages,omitempty" bun:",nullzero"`
|
||||
FilteredLanguages []string `json:"filteredLanguage,omitempty" bun:",nullzero"`
|
||||
Locale string `json:"locale" bun:",nullzero"`
|
||||
LastEmailedAt time.Time `json:"lastEmailedAt,omitempty" bun:",nullzero"`
|
||||
ConfirmationToken string `json:"confirmationToken,omitempty" bun:",nullzero"`
|
||||
|
|
|
@ -414,13 +414,13 @@ func (c *Converter) AccountToAdminAPIAccount(ctx context.Context, a *gtsmodel.Ac
|
|||
email = user.UnconfirmedEmail
|
||||
}
|
||||
|
||||
if i := user.CurrentSignInIP.String(); i != "<nil>" {
|
||||
if i := user.SignUpIP.String(); i != "<nil>" {
|
||||
ip = &i
|
||||
}
|
||||
|
||||
locale = user.Locale
|
||||
if a.Settings.Reason != "" {
|
||||
inviteRequest = &a.Settings.Reason
|
||||
if user.Reason != "" {
|
||||
inviteRequest = &user.Reason
|
||||
}
|
||||
|
||||
if *user.Admin {
|
||||
|
@ -1003,7 +1003,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
|
|||
Version: config.GetSoftwareVersion(),
|
||||
Languages: config.GetInstanceLanguages().TagStrs(),
|
||||
Registrations: config.GetAccountsRegistrationOpen(),
|
||||
ApprovalRequired: config.GetAccountsApprovalRequired(),
|
||||
ApprovalRequired: true, // approval always required
|
||||
InvitesEnabled: false, // todo: not supported yet
|
||||
MaxTootChars: uint(config.GetStatusesMaxChars()),
|
||||
Rules: c.InstanceRulesToAPIRules(i.Rules),
|
||||
|
@ -1172,8 +1172,8 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
|
|||
|
||||
// registrations
|
||||
instance.Registrations.Enabled = config.GetAccountsRegistrationOpen()
|
||||
instance.Registrations.ApprovalRequired = config.GetAccountsApprovalRequired()
|
||||
instance.Registrations.Message = nil // todo: not implemented
|
||||
instance.Registrations.ApprovalRequired = true // always required
|
||||
instance.Registrations.Message = nil // todo: not implemented
|
||||
|
||||
// contact
|
||||
instance.Contact.Email = i.ContactEmail
|
||||
|
|
|
@ -1386,7 +1386,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
|||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": "118.44.18.196",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -1443,7 +1443,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
|||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": "89.122.255.1",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -1489,7 +1489,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
|||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": "89.122.255.1",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -1558,7 +1558,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
|||
"domain": null,
|
||||
"created_at": "2022-06-04T13:12:00.000Z",
|
||||
"email": "tortle.dude@example.org",
|
||||
"ip": "118.44.18.196",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -1880,7 +1880,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
|||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": "89.122.255.1",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
@ -1926,7 +1926,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
|||
"domain": null,
|
||||
"created_at": "2022-05-17T13:10:59.000Z",
|
||||
"email": "admin@example.org",
|
||||
"ip": "89.122.255.1",
|
||||
"ip": null,
|
||||
"ips": [],
|
||||
"locale": "en",
|
||||
"invite_request": null,
|
||||
|
|
|
@ -348,3 +348,42 @@ func FilterContexts(contexts []apimodel.FilterContext) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateAccount checks through all the prerequisites for
|
||||
// creating a new account, according to the provided form.
|
||||
// If the account isn't eligible, an error will be returned.
|
||||
//
|
||||
// Side effect: normalizes the provided language tag for the user's locale.
|
||||
func CreateAccount(form *apimodel.AccountCreateRequest) error {
|
||||
if form == nil {
|
||||
return errors.New("form was nil")
|
||||
}
|
||||
|
||||
if !config.GetAccountsRegistrationOpen() {
|
||||
return errors.New("registration is not open for this server")
|
||||
}
|
||||
|
||||
if err := Username(form.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := Email(form.Email); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := Password(form.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !form.Agreement {
|
||||
return errors.New("agreement to terms and conditions not given")
|
||||
}
|
||||
|
||||
locale, err := Language(form.Locale)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
form.Locale = locale
|
||||
|
||||
return SignUpReason(form.Reason, config.GetAccountsReasonRequired())
|
||||
}
|
||||
|
|
|
@ -56,18 +56,84 @@ func (m *Module) confirmEmailGETHandler(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Get user but don't confirm yet.
|
||||
user, errWithCode := m.processor.User().EmailGetUserForConfirmToken(c.Request.Context(), token)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// They may have already confirmed before
|
||||
// and are visiting the link again for
|
||||
// whatever reason. This is fine, just make
|
||||
// sure we have an email address to show them.
|
||||
email := user.UnconfirmedEmail
|
||||
if email == "" {
|
||||
// Already confirmed, take
|
||||
// that address instead.
|
||||
email = user.Email
|
||||
}
|
||||
|
||||
// Serve page where user can click button
|
||||
// to POST confirmation to same endpoint.
|
||||
page := apiutil.WebPage{
|
||||
Template: "confirm_email.tmpl",
|
||||
Instance: instance,
|
||||
Extra: map[string]any{
|
||||
"email": email,
|
||||
"username": user.Account.Username,
|
||||
"token": token,
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
func (m *Module) confirmEmailPOSTHandler(c *gin.Context) {
|
||||
instance, errWithCode := m.processor.InstanceGetV1(c.Request.Context())
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// If there's no token in the query,
|
||||
// just serve the 404 web handler.
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
errWithCode := gtserror.NewErrorNotFound(errors.New(http.StatusText(http.StatusNotFound)))
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Confirm email address for real this time.
|
||||
user, errWithCode := m.processor.User().EmailConfirm(c.Request.Context(), token)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve page informing user that their
|
||||
// email address is now confirmed.
|
||||
page := apiutil.WebPage{
|
||||
Template: "confirmed.tmpl",
|
||||
Template: "confirmed_email.tmpl",
|
||||
Instance: instance,
|
||||
Extra: map[string]any{
|
||||
"email": user.Email,
|
||||
"username": user.Account.Username,
|
||||
"token": token,
|
||||
"approved": *user.Approved,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ Disallow: /oauth/
|
|||
Disallow: /check_your_email
|
||||
Disallow: /wait_for_approval
|
||||
Disallow: /account_disabled
|
||||
Disallow: /signup
|
||||
|
||||
# Well-known endpoints.
|
||||
Disallow: /.well-known/
|
||||
|
|
138
internal/web/signup.go
Normal file
138
internal/web/signup.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
func (m *Module) signupGETHandler(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// We'll need the instance later, and we can also use it
|
||||
// before then to make it easier to return a web error.
|
||||
instance, errWithCode := m.processor.InstanceGetV1(ctx)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
page := apiutil.WebPage{
|
||||
Template: "sign-up.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Extra: map[string]any{
|
||||
"reasonRequired": config.GetAccountsReasonRequired(),
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
||||
|
||||
func (m *Module) signupPOSTHandler(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// We'll need the instance later, and we can also use it
|
||||
// before then to make it easier to return a web error.
|
||||
instance, errWithCode := m.processor.InstanceGetV1(ctx)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
// Return instance we already got from the db,
|
||||
// don't try to fetch it again when erroring.
|
||||
instanceGet := func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) {
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
// We only serve text/html at this endpoint.
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.TextHTML); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.AccountCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validate.CreateAccount(form); err != nil {
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
signUpIP := net.ParseIP(clientIP)
|
||||
if signUpIP == nil {
|
||||
err := errors.New("ip address could not be parsed from request")
|
||||
apiutil.WebErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), instanceGet)
|
||||
return
|
||||
}
|
||||
form.IP = signUpIP
|
||||
|
||||
// We have all the info we need, call account create
|
||||
// (this will also trigger side effects like sending emails etc).
|
||||
user, errWithCode := m.processor.Account().Create(
|
||||
c.Request.Context(),
|
||||
// nil to use
|
||||
// instance app.
|
||||
nil,
|
||||
form,
|
||||
)
|
||||
if errWithCode != nil {
|
||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve a page informing the
|
||||
// user that they've signed up.
|
||||
page := apiutil.WebPage{
|
||||
Template: "signed-up.tmpl",
|
||||
Instance: instance,
|
||||
OGMeta: apiutil.OGBase(instance),
|
||||
Extra: map[string]any{
|
||||
"email": user.UnconfirmedEmail,
|
||||
"username": user.Account.Username,
|
||||
},
|
||||
}
|
||||
|
||||
apiutil.TemplateWebPage(c, page)
|
||||
}
|
|
@ -49,6 +49,7 @@ const (
|
|||
settingsPanelGlob = settingsPathPrefix + "/*panel"
|
||||
userPanelPath = settingsPathPrefix + "/user"
|
||||
adminPanelPath = settingsPathPrefix + "/admin"
|
||||
signupPath = "/signup"
|
||||
|
||||
cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||
cacheControlNoCache = "no-cache" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives
|
||||
|
@ -115,10 +116,13 @@ func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
|
|||
r.AttachHandler(http.MethodGet, customCSSPath, m.customCSSGETHandler)
|
||||
r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler)
|
||||
r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)
|
||||
r.AttachHandler(http.MethodPost, confirmEmailPath, m.confirmEmailPOSTHandler)
|
||||
r.AttachHandler(http.MethodGet, robotsPath, m.robotsGETHandler)
|
||||
r.AttachHandler(http.MethodGet, aboutPath, m.aboutGETHandler)
|
||||
r.AttachHandler(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler)
|
||||
r.AttachHandler(http.MethodGet, tagsPath, m.tagGETHandler)
|
||||
r.AttachHandler(http.MethodGet, signupPath, m.signupGETHandler)
|
||||
r.AttachHandler(http.MethodPost, signupPath, m.signupPOSTHandler)
|
||||
|
||||
// Attach redirects from old endpoints to current ones for backwards compatibility
|
||||
r.AttachHandler(http.MethodGet, "/auth/edit", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, userPanelPath) })
|
||||
|
|
|
@ -6,7 +6,6 @@ EXPECT=$(cat << "EOF"
|
|||
{
|
||||
"account-domain": "peepee",
|
||||
"accounts-allow-custom-css": true,
|
||||
"accounts-approval-required": false,
|
||||
"accounts-custom-css-length": 5000,
|
||||
"accounts-reason-required": false,
|
||||
"accounts-registration-open": true,
|
||||
|
@ -224,7 +223,6 @@ GTS_INSTANCE_LANGUAGES="nl,en-gb" \
|
|||
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=true \
|
||||
GTS_ACCOUNTS_CUSTOM_CSS_LENGTH=5000 \
|
||||
GTS_ACCOUNTS_REGISTRATION_OPEN=true \
|
||||
GTS_ACCOUNTS_APPROVAL_REQUIRED=false \
|
||||
GTS_ACCOUNTS_REASON_REQUIRED=false \
|
||||
GTS_MEDIA_IMAGE_MAX_SIZE=420 \
|
||||
GTS_MEDIA_VIDEO_MAX_SIZE=420 \
|
||||
|
|
|
@ -89,7 +89,6 @@ var testDefaults = config.Configuration{
|
|||
},
|
||||
|
||||
AccountsRegistrationOpen: true,
|
||||
AccountsApprovalRequired: true,
|
||||
AccountsReasonRequired: true,
|
||||
AccountsAllowCustomCSS: true,
|
||||
AccountsCustomCSSLength: 10000,
|
||||
|
|
|
@ -20,6 +20,7 @@ package testrig
|
|||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// NewEmailSender returns a noop email sender that won't make any remote calls.
|
||||
|
@ -38,6 +39,10 @@ func NewEmailSender(templateBaseDir string, sentEmails map[string]string) email.
|
|||
sendCallback = func(toAddress string, message string) {
|
||||
sentEmails[toAddress] = message
|
||||
}
|
||||
} else {
|
||||
sendCallback = func(toAddress string, message string) {
|
||||
log.Infof(nil, "Sent email to %s: %s", toAddress, message)
|
||||
}
|
||||
}
|
||||
|
||||
s, err := email.NewNoopSender(sendCallback)
|
||||
|
|
|
@ -100,6 +100,12 @@ func NewTestTokens() map[string]*gtsmodel.Token {
|
|||
// NewTestClients returns a map of Clients keyed according to which account they are used by.
|
||||
func NewTestClients() map[string]*gtsmodel.Client {
|
||||
clients := map[string]*gtsmodel.Client{
|
||||
"instance_application": {
|
||||
ID: "01AY6P665V14JJR0AFVRT7311Y",
|
||||
Secret: "baedee87-6d00-4cf5-87b9-4d78ee58ef01",
|
||||
Domain: "http://localhost:8080",
|
||||
UserID: "",
|
||||
},
|
||||
"admin_account": {
|
||||
ID: "01F8MGWSJCND9BWBD4WGJXBM93",
|
||||
Secret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a",
|
||||
|
@ -125,6 +131,15 @@ func NewTestClients() map[string]*gtsmodel.Client {
|
|||
// NewTestApplications returns a map of applications keyed to which number application they are.
|
||||
func NewTestApplications() map[string]*gtsmodel.Application {
|
||||
apps := map[string]*gtsmodel.Application{
|
||||
"instance_application": {
|
||||
ID: "01HT5P2YHDMPAAD500NDAY8JW1",
|
||||
Name: "localhost:8080 instance application",
|
||||
Website: "http://localhost:8080",
|
||||
RedirectURI: "http://localhost:8080",
|
||||
ClientID: "01AY6P665V14JJR0AFVRT7311Y", // instance account ID
|
||||
ClientSecret: "baedee87-6d00-4cf5-87b9-4d78ee58ef01",
|
||||
Scopes: "write:accounts",
|
||||
},
|
||||
"admin_account": {
|
||||
ID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
Name: "superseriousbusiness",
|
||||
|
@ -167,14 +182,8 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
|||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
SignUpIP: net.ParseIP("199.222.111.89"),
|
||||
UpdatedAt: time.Time{},
|
||||
CurrentSignInAt: time.Time{},
|
||||
CurrentSignInIP: nil,
|
||||
LastSignInAt: time.Time{},
|
||||
LastSignInIP: nil,
|
||||
SignInCount: 0,
|
||||
InviteID: "",
|
||||
ChosenLanguages: []string{},
|
||||
FilteredLanguages: []string{},
|
||||
Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
|
||||
Locale: "en",
|
||||
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
LastEmailedAt: time.Time{},
|
||||
|
@ -195,16 +204,9 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
|||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
||||
CreatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
||||
SignUpIP: net.ParseIP("89.22.189.19"),
|
||||
SignUpIP: nil,
|
||||
UpdatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
||||
CurrentSignInAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
CurrentSignInIP: net.ParseIP("89.122.255.1"),
|
||||
LastSignInAt: TimeMustParse("2022-06-03T13:12:00Z"),
|
||||
LastSignInIP: net.ParseIP("89.122.255.1"),
|
||||
SignInCount: 78,
|
||||
InviteID: "",
|
||||
ChosenLanguages: []string{"en"},
|
||||
FilteredLanguages: []string{},
|
||||
Locale: "en",
|
||||
CreatedByApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||
LastEmailedAt: TimeMustParse("2022-06-03T13:12:00Z"),
|
||||
|
@ -225,16 +227,10 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
|||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
||||
CreatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
||||
SignUpIP: net.ParseIP("59.99.19.172"),
|
||||
SignUpIP: nil,
|
||||
UpdatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
||||
CurrentSignInAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
CurrentSignInIP: net.ParseIP("88.234.118.16"),
|
||||
LastSignInAt: TimeMustParse("2022-06-03T13:12:00Z"),
|
||||
LastSignInIP: net.ParseIP("147.111.231.154"),
|
||||
SignInCount: 9,
|
||||
InviteID: "",
|
||||
ChosenLanguages: []string{"en"},
|
||||
FilteredLanguages: []string{},
|
||||
Reason: "I wanna be on this damned webbed site so bad! Please! Wow",
|
||||
Locale: "en",
|
||||
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
LastEmailedAt: TimeMustParse("2022-06-02T13:12:00Z"),
|
||||
|
@ -255,16 +251,9 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
|||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
||||
CreatedAt: TimeMustParse("2022-05-23T13:12:00Z"),
|
||||
SignUpIP: net.ParseIP("59.99.19.172"),
|
||||
SignUpIP: nil,
|
||||
UpdatedAt: TimeMustParse("2022-05-23T13:12:00Z"),
|
||||
CurrentSignInAt: TimeMustParse("2022-06-05T13:12:00Z"),
|
||||
CurrentSignInIP: net.ParseIP("118.44.18.196"),
|
||||
LastSignInAt: TimeMustParse("2022-06-06T13:12:00Z"),
|
||||
LastSignInIP: net.ParseIP("198.98.21.15"),
|
||||
SignInCount: 9,
|
||||
InviteID: "",
|
||||
ChosenLanguages: []string{"en"},
|
||||
FilteredLanguages: []string{},
|
||||
Locale: "en",
|
||||
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
LastEmailedAt: TimeMustParse("2022-06-06T13:12:00Z"),
|
||||
|
@ -664,7 +653,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
AccountID: "01F8MH0BBE4FHXPH513MBVFHB0",
|
||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
|
||||
Privacy: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
|
@ -675,7 +663,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
|
||||
UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
|
||||
Reason: "",
|
||||
Privacy: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
|
@ -686,7 +673,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
|
||||
UpdatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
|
||||
Reason: "I wanna be on this damned webbed site so bad! Please! Wow",
|
||||
Privacy: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
|
@ -697,7 +683,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
|||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
Reason: "",
|
||||
Privacy: gtsmodel.VisibilityFollowersOnly,
|
||||
Sensitive: util.Ptr(true),
|
||||
Language: "fr",
|
||||
|
@ -2428,6 +2413,15 @@ func NewTestNotifications() map[string]*gtsmodel.Notification {
|
|||
StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
Read: util.Ptr(false),
|
||||
},
|
||||
"new_signup": {
|
||||
ID: "01HTM9TETMB3YQCBKZ7KD4KV02",
|
||||
NotificationType: gtsmodel.NotificationSignup,
|
||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
OriginAccountID: "01F8MH0BBE4FHXPH513MBVFHB0",
|
||||
StatusID: "",
|
||||
Read: util.Ptr(false),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -407,6 +407,57 @@ pre, pre[class*="language-"] {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Forms and sign-in / sign-up / confirm pages.
|
||||
*/
|
||||
section.with-form {
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
p {
|
||||
/*
|
||||
We use gap so we don't
|
||||
need top + bottom margins.
|
||||
*/
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label, input {
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
|
||||
.labelinput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 0.4rem;
|
||||
|
||||
& > input {
|
||||
height: 100%;
|
||||
width: 5%;
|
||||
min-width: 1.2rem;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
/* Visually separate buttons a bit */
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***********************************
|
||||
***** SECTION 4: SHAMEFUL MESS *****
|
||||
************************************/
|
||||
|
@ -419,33 +470,8 @@ pre, pre[class*="language-"] {
|
|||
|
||||
/*
|
||||
Below section stylings are used
|
||||
in transient/error templates.
|
||||
in transient pages + error templates.
|
||||
*/
|
||||
section.sign-in {
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
|
||||
label, input {
|
||||
padding-left: 0.2rem;
|
||||
}
|
||||
|
||||
.labelinput {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.error {
|
||||
word-break: break-word;
|
||||
|
@ -470,25 +496,6 @@ section.oob-token {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: This is only used in the "finalize"
|
||||
template for new signups; move this elsewhere
|
||||
when that stuff is finished up.
|
||||
*/
|
||||
.callout {
|
||||
margin: 1.5rem 0;
|
||||
border: .05rem solid $border-accent;
|
||||
border-radius: .2rem;
|
||||
padding: 0 .6rem .6rem;
|
||||
.callout-title {
|
||||
margin: 0 -.6rem;
|
||||
padding: .6rem;
|
||||
font-weight: bold;
|
||||
background-color: $border-accent;
|
||||
color: $gray1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: list and blocklist are only used
|
||||
in settings panel and on blocklist page;
|
||||
|
|
|
@ -59,37 +59,26 @@
|
|||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "registrationLimits" -}}
|
||||
{{- if .instance.Registrations -}}
|
||||
Registration is enabled; new signups can be submitted to this instance.<br/>
|
||||
{{- if .instance.ApprovalRequired -}}
|
||||
Admin approval is required for new registrations.
|
||||
{{- else -}}
|
||||
Admin approval is not required for registrations; new signups will be automatically approved (pending email confirmation).
|
||||
{{- end -}}
|
||||
{{- else -}}
|
||||
Registration is disabled; new signups are currently closed for this instance.
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "customCSSLimits" -}}
|
||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/settings/#custom-css" target="_blank" rel="noopener noreferrer">Custom CSS</a> is
|
||||
{{- if .instance.Configuration.Accounts.AllowCustomCSS -}}
|
||||
Users are allowed to set <a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/" target="_blank" rel="noopener noreferrer">Custom CSS</a> for their profiles.
|
||||
<b>enabled</b>
|
||||
{{- else -}}
|
||||
<a href="https://docs.gotosocial.org/en/latest/user_guide/custom_css/" target="_blank" rel="noopener noreferrer">Custom CSS</a> is not enabled for user profiles.
|
||||
</b>disabled</b>
|
||||
{{- end -}}
|
||||
on account profiles.
|
||||
{{- end -}}
|
||||
|
||||
{{- define "statusLimits" -}}
|
||||
Statuses can contain up to
|
||||
{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and
|
||||
{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments.
|
||||
Statuses can contain up to
|
||||
<b>{{- .instance.Configuration.Statuses.MaxCharacters }} characters</b>, and
|
||||
<b>{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments</b>.
|
||||
{{- end -}}
|
||||
|
||||
{{- define "pollLimits" -}}
|
||||
Polls can have up to
|
||||
{{- .instance.Configuration.Polls.MaxOptions }} options, with
|
||||
{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option.
|
||||
Polls can have up to
|
||||
<b>{{- .instance.Configuration.Polls.MaxOptions }} options</b>, with
|
||||
<b>{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option</b>.
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
|
@ -102,6 +91,7 @@ Polls can have up to
|
|||
<li><a href="#contact">Contact</a></li>
|
||||
<li><a href="#features">Features</a></li>
|
||||
<li><a href="#languages">Languages</a></li>
|
||||
<li><a href="#signup">Register an Account on {{ .instance.Title -}}</li>
|
||||
<li><a href="#rules">Rules</a></li>
|
||||
<li><a href="#terms">Terms and Conditions</a></li>
|
||||
<li><a href="#moderated-servers">Moderated Servers</a></li>
|
||||
|
@ -145,10 +135,9 @@ Polls can have up to
|
|||
<h3 id="features">Instance Features</h3>
|
||||
<div class="about-section-contents">
|
||||
<ul>
|
||||
<li>{{- template "registrationLimits" . -}}</li>
|
||||
<li>{{- template "customCSSLimits" . -}}</li>
|
||||
<li>{{- template "statusLimits" . -}}</li>
|
||||
<li>{{- template "pollLimits" . -}}</li>
|
||||
<li>{{- template "customCSSLimits" . -}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -160,6 +149,7 @@ Polls can have up to
|
|||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{- include "index_register.tmpl" . | indent 1 }}
|
||||
<section class="about-section" role="region" aria-labelledby="rules">
|
||||
<h3 id="rules">Instance Rules</h3>
|
||||
<div class="about-section-contents">
|
||||
|
|
|
@ -19,22 +19,25 @@
|
|||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<form action="/oauth/authorize" method="POST">
|
||||
<h1>Hi {{ .user -}}!</h1>
|
||||
<p>
|
||||
Application
|
||||
{{- if .appwebsite }}
|
||||
<a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
|
||||
{{- else }}
|
||||
<b>{{- .appname -}}</b>
|
||||
{{- end }}
|
||||
would like to perform actions on your behalf, with scope
|
||||
<em>{{- .scope -}}</em>.
|
||||
</p>
|
||||
<p>
|
||||
To continue, the application will redirect to: <code>{{- .redirect -}}</code>
|
||||
</p>
|
||||
<button type="submit" style="width:200px;">Allow</button>
|
||||
</form>
|
||||
<section class="with-form" aria-labelledby="authorize">
|
||||
<h2 id="authorize">Authorize app</h2>
|
||||
<form action="/oauth/authorize" method="POST">
|
||||
<p>Hi <b>{{- .user -}}</b>!</p>
|
||||
<p>
|
||||
Application
|
||||
{{- if .appwebsite }}
|
||||
<a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
|
||||
{{- else }}
|
||||
<b>{{- .appname -}}</b>
|
||||
{{- end }}
|
||||
would like to perform actions on your behalf, with scope
|
||||
<em>{{- .scope -}}</em>.
|
||||
</p>
|
||||
<p>
|
||||
To continue, the application will redirect to: <code>{{- .redirect -}}</code>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-success">Allow</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{{- end }}
|
33
web/template/confirm_email.tmpl
Normal file
33
web/template/confirm_email.tmpl
Normal file
|
@ -0,0 +1,33 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section class="with-form" aria-labelledby="confirm">
|
||||
<h2 id="confirm">Confirm email address</h2>
|
||||
<form action="/confirm_email?token={{ .token }}" method="POST">
|
||||
<p>
|
||||
Hi <b>{{- .username -}}</b>!
|
||||
Please click the button to confirm your email address <b>{{- .email -}}</b>.
|
||||
</p>
|
||||
<button type="submit" class="btn btn-success">Confirm</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{{- end }}
|
|
@ -19,9 +19,12 @@
|
|||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section>
|
||||
<h1>Email Address Confirmed</h1>
|
||||
<p>Thanks {{ .username -}}! Your email address <b>{{- .email -}}</b> has been confirmed.<p>
|
||||
<section aria-labelledby="confirmed">
|
||||
<h2 id="confirmed">Email address confirmed</h2>
|
||||
<p>Email address <b>{{- .email -}}</b> is now confirmed!</p>
|
||||
{{- if not .approved }}
|
||||
<p>Once an admin has approved your sign-up, you will be able to log in and use your account.</p>
|
||||
{{- end }}
|
||||
</section>
|
||||
</main>
|
||||
{{- end }}
|
|
@ -17,12 +17,14 @@
|
|||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
Hello {{.Username}}!
|
||||
Hello {{ .Username -}}!
|
||||
|
||||
You are receiving this mail because you've requested an account on {{.InstanceURL}}.
|
||||
You are receiving this mail because you've requested an account on {{ .InstanceURL -}}.
|
||||
|
||||
We just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:
|
||||
To use your account, you must confirm that this is your email address.
|
||||
|
||||
{{.ConfirmLink}}
|
||||
To confirm your email, paste the following in your browser's address bar:
|
||||
|
||||
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{.InstanceURL}}
|
||||
{{ .ConfirmLink }}
|
||||
|
||||
If you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of {{ .InstanceURL -}}.
|
||||
|
|
32
web/template/email_new_signup.tmpl
Normal file
32
web/template/email_new_signup.tmpl
Normal file
|
@ -0,0 +1,32 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
Hello moderator of {{ .InstanceName }} ({{ .InstanceURL }})!
|
||||
|
||||
Someone has submitted a new account sign-up to your instance.
|
||||
|
||||
They provided the following details:
|
||||
|
||||
Email address: {{ .SignupEmail }}
|
||||
Username: {{ .SignupUsername }}
|
||||
{{- if .SignupReason }}
|
||||
Reason: {{ .SignupReason }}
|
||||
{{- end }}
|
||||
|
||||
To view the sign-up, paste the following link into your browser: {{ .SignupURL }}
|
|
@ -19,29 +19,35 @@
|
|||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<form action="/oauth/finalize" method="POST">
|
||||
<h1>Hi {{ .name -}}!</h1>
|
||||
<p>
|
||||
You are about to sign-up to {{ .instance.Title -}}.
|
||||
To ensure the best experience for you, we need you to provide some additional details.
|
||||
</p>
|
||||
<div class="callout">
|
||||
<p class="callout-title">Important</p>
|
||||
<p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
|
||||
</div>
|
||||
<div class="labelinput">
|
||||
<label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
name="username"
|
||||
required
|
||||
placeholder="Please enter your desired username"
|
||||
value="{{- .preferredUsername -}}"
|
||||
>
|
||||
</div>
|
||||
<input type="hidden" name="name" value="{{- .name -}}">
|
||||
<button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
|
||||
</form>
|
||||
<section class="with-form" aria-labelledby="finalize">
|
||||
<h2 id="finalize">Finalize sign-in to {{ .instance.Title -}}</h2>
|
||||
<form action="/oauth/finalize" method="POST">
|
||||
<p>
|
||||
Hi <b>{{- .name -}}</b>!
|
||||
</p>
|
||||
<p>
|
||||
You are about to create an account on <b>{{- .instance.Title -}}</b>.
|
||||
To finish the process, you must select your username.
|
||||
</p>
|
||||
<div class="labelinput">
|
||||
<label for="username">
|
||||
Username (lowercase a-z, numbers, and underscores; max 64 characters).<br/>
|
||||
<small>Your username will be part of your fediverse handle, and cannot be changed later, so choose thoughtfully!</small>
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
placeholder="Please enter your desired username"
|
||||
pattern="^[a-z0-9_]{1,64}$"
|
||||
title="lowercase a-z, numbers, and underscores; max 64 characters"
|
||||
value="{{- .preferredUsername -}}"
|
||||
>
|
||||
</div>
|
||||
<input type="hidden" name="name" value="{{- .name -}}">
|
||||
<button type="submit" class="btn btn-success">Submit</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{{- end }}
|
|
@ -35,6 +35,7 @@
|
|||
</div>
|
||||
</section>
|
||||
{{- include "index_what_is_this.tmpl" . | indent 1 }}
|
||||
{{- include "index_register.tmpl" . | indent 1 }}
|
||||
{{- include "index_apps.tmpl" . | indent 1 }}
|
||||
</main>
|
||||
{{- end }}
|
|
@ -22,8 +22,9 @@
|
|||
<h3 id="apps">Client applications</h3>
|
||||
<div class="about-section-contents">
|
||||
<p>
|
||||
Have an account on this instance and want to log in?
|
||||
GoToSocial does not provide its own webclient, but implements the Mastodon client API.
|
||||
You can use this server through a variety of other clients:
|
||||
You can use a variety of clients to log in to your account here:
|
||||
</p>
|
||||
<ul class="applist nodot" role="group">
|
||||
<li class="applist-entry">
|
||||
|
|
41
web/template/index_register.tmpl
Normal file
41
web/template/index_register.tmpl
Normal file
|
@ -0,0 +1,41 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{- define "registrationLimits" -}}
|
||||
New account registration is currently
|
||||
{{- if .instance.Registrations -}}
|
||||
<b>open</b>.
|
||||
{{- else -}}
|
||||
<b>closed</b>.
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- with . }}
|
||||
<section class="about-section" role="region" aria-labelledby="signup">
|
||||
<h3 id="signup">Register an Account on {{ .instance.Title -}}</h3>
|
||||
<div class="about-section-contents">
|
||||
<p>{{- template "registrationLimits" . -}}</p>
|
||||
{{- if .instance.Registrations }}
|
||||
<p>To register a new account, please first read the <a href="/about#rules">rules</a> and <a href="/about#terms">terms</a>.</p>
|
||||
<p>Then, use the <a href="/signup">sign-up page</a> to register an account.</p>
|
||||
<p>Manual admin approval is <b>required</b> for new accounts.</p>
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{- end }}
|
|
@ -44,7 +44,7 @@
|
|||
<p>
|
||||
You can join the fediverse by running your own instance of an ActivityPub software,
|
||||
or by finding an existing instance that aligns with your values and expectations,
|
||||
and registering an account there.
|
||||
and registering an account.
|
||||
</p>
|
||||
<p>
|
||||
To help you find an instance that suits you, you can try one of the following tools:
|
||||
|
@ -53,6 +53,9 @@
|
|||
<li><a href="https://fediverse.observer" rel="nofollow noreferrer noopener" target="_blank">Fediverse Observer (opens in a new tab)</a></li>
|
||||
<li><a href="https://fedidb.org/network" rel="nofollow noreferrer noopener" target="_blank">FediDB (opens in a new tab)</a></li>
|
||||
</ul>
|
||||
{{- if .instance.Registrations }}
|
||||
<p>Or, just <a href="#signup">register for an account on this instance</a>!</p>
|
||||
{{- end }}
|
||||
</div>
|
||||
</section>
|
||||
{{- end }}
|
|
@ -20,7 +20,7 @@
|
|||
{{- with . }}
|
||||
<main>
|
||||
<section class="oob-token">
|
||||
<h1>Hi {{ .user -}}!</h1>
|
||||
<h1>Hi <b>{{- .user -}}</b>!</h1>
|
||||
<p>Here's your out-of-band token with scope "<em>{{- .scope -}}</em>", use it wisely:</p>
|
||||
<code>{{- .oobToken -}}</code>
|
||||
</section>
|
||||
|
|
|
@ -19,16 +19,16 @@
|
|||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section class="sign-in" aria-labelledby="sign-in">
|
||||
<section class="with-form" aria-labelledby="sign-in">
|
||||
<h2 id="sign-in">Sign in</h2>
|
||||
<form action="/auth/sign_in" method="POST">
|
||||
<div class="labelinput">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" class="form-control" name="username" required placeholder="Please enter your email address">
|
||||
<input type="email" name="username" required placeholder="Please enter your email address">
|
||||
</div>
|
||||
<div class="labelinput">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" required placeholder="Please enter your password">
|
||||
<input type="password" name="password" required placeholder="Please enter your password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Sign in</button>
|
||||
</form>
|
||||
|
|
93
web/template/sign-up.tmpl
Normal file
93
web/template/sign-up.tmpl
Normal file
|
@ -0,0 +1,93 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section class="with-form" aria-labelledby="sign-up">
|
||||
<h2 id="sign-up">Sign up for an account on {{ .instance.Title -}}</h2>
|
||||
<form action="/signup" method="POST">
|
||||
<div class="labelinput">
|
||||
<label for="email">Email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
placeholder="Email address"
|
||||
>
|
||||
</div>
|
||||
<div class="labelinput">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
placeholder="Please enter your desired password"
|
||||
>
|
||||
</div>
|
||||
<div class="labelinput">
|
||||
<label for="username">
|
||||
Username (lowercase a-z, numbers, and underscores; max 64 characters).<br/>
|
||||
<small>Your username will be part of your fediverse handle, and cannot be changed later, so choose thoughtfully!</small>
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
placeholder="Please enter your desired username"
|
||||
pattern="^[a-z0-9_]{1,64}$"
|
||||
title="lowercase a-z, numbers, and underscores; max 64 characters"
|
||||
>
|
||||
</div>
|
||||
{{- if .reasonRequired }}
|
||||
<div class="labelinput">
|
||||
<label for="reason">
|
||||
Reason you want to join {{ .instance.Title }} (40-500 characters).<br/>
|
||||
<small>The admin(s) will use this text to decide whether or not to approve your sign-up.</small>
|
||||
</label>
|
||||
<textarea
|
||||
id="reason"
|
||||
name="reason"
|
||||
required
|
||||
placeholder="Enter a few sentences about why you want to join this instance. If you know someone on the instance already, you may want to mention them here. You might want to link to any other accounts you have elsewhere too."
|
||||
rows="8"
|
||||
minlength="40"
|
||||
maxlength="500"
|
||||
title="40-500 characters"
|
||||
></textarea>
|
||||
</div>
|
||||
{{- end }}
|
||||
<div class="checkbox">
|
||||
<label for="agreement">I have read and accept the <a href="/about#terms">terms and conditions</a> of {{ .instance.Title }}, and I agree to abide by the <a href="/about#rules">instance rules</a>.</label>
|
||||
<input
|
||||
id="agreement"
|
||||
type="checkbox"
|
||||
name="agreement"
|
||||
required
|
||||
value="true"
|
||||
>
|
||||
</div>
|
||||
<input type="hidden" name="locale" value="en">
|
||||
<button type="submit" class="btn btn-success">Submit</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
{{- end }}
|
30
web/template/signed-up.tmpl
Normal file
30
web/template/signed-up.tmpl
Normal file
|
@ -0,0 +1,30 @@
|
|||
{{- /*
|
||||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ -}}
|
||||
|
||||
{{- with . }}
|
||||
<main>
|
||||
<section aria-labelledby="signed-up">
|
||||
<h2 id="signed-up">Thanks for signing up to {{ .instance.Title -}}!</h2>
|
||||
<p>Hi <b>{{- .username -}}</b>!</p>
|
||||
<p>Your sign-up has been registered, and a confirmation email has been sent to <b>{{- .email -}}</b>.<p>
|
||||
<p>Please check your email inbox and click the link to confirm your email.</p>
|
||||
<p>Once an admin has approved your sign-up, you will be able to log in and use your account.</p>
|
||||
</section>
|
||||
</main>
|
||||
{{- end }}
|
Loading…
Reference in a new issue