mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 10:51:00 +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.Approved = func() *bool { a := true; return &a }()
|
||||||
user.Email = user.UnconfirmedEmail
|
user.Email = user.UnconfirmedEmail
|
||||||
user.ConfirmedAt = time.Now()
|
user.ConfirmedAt = time.Now()
|
||||||
|
user.SignUpIP = nil
|
||||||
return state.DB.UpdateUser(
|
return state.DB.UpdateUser(
|
||||||
ctx, user,
|
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)
|
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
|
// Get the instance account
|
||||||
// (we'll need this later).
|
// (we'll need this later).
|
||||||
instanceAccount, err := dbService.GetInstanceAccount(ctx, "")
|
instanceAccount, err := dbService.GetInstanceAccount(ctx, "")
|
||||||
|
|
|
@ -439,8 +439,8 @@ definitions:
|
||||||
x-go-name: ID
|
x-go-name: ID
|
||||||
invite_request:
|
invite_request:
|
||||||
description: |-
|
description: |-
|
||||||
The reason given when requesting an invite.
|
The reason given when signing up.
|
||||||
Null if not known / remote account.
|
Null if no reason / remote account.
|
||||||
example: Pleaaaaaaaaaaaaaaase!!
|
example: Pleaaaaaaaaaaaaaaase!!
|
||||||
type: string
|
type: string
|
||||||
x-go-name: InviteRequest
|
x-go-name: InviteRequest
|
||||||
|
@ -1842,13 +1842,14 @@ definitions:
|
||||||
type:
|
type:
|
||||||
description: |-
|
description: |-
|
||||||
The type of event that resulted in the notification.
|
The type of event that resulted in the notification.
|
||||||
follow = Someone followed you
|
follow = Someone followed you. `account` will be set.
|
||||||
follow_request = Someone requested to follow you
|
follow_request = Someone requested to follow you. `account` will be set.
|
||||||
mention = Someone mentioned you in their status
|
mention = Someone mentioned you in their status. `status` will be set. `account` will be set.
|
||||||
reblog = Someone boosted one of your statuses
|
reblog = Someone boosted one of your statuses. `status` will be set. `account` will be set.
|
||||||
favourite = Someone favourited one of your statuses
|
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
|
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 = 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
|
type: string
|
||||||
x-go-name: Type
|
x-go-name: Type
|
||||||
title: Notification represents a notification of an event relevant to the user.
|
title: Notification represents a notification of an event relevant to the user.
|
||||||
|
@ -2773,6 +2774,8 @@ paths:
|
||||||
description: not found
|
description: not found
|
||||||
"406":
|
"406":
|
||||||
description: not acceptable
|
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":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
|
|
|
@ -14,11 +14,6 @@
|
||||||
# Default: true
|
# Default: true
|
||||||
accounts-registration-open: 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)?
|
# 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]
|
# Options: [true, false]
|
||||||
# Default: true
|
# Default: true
|
||||||
|
|
|
@ -411,11 +411,6 @@ instance-inject-mastodon-version: false
|
||||||
# Default: true
|
# Default: true
|
||||||
accounts-registration-open: 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)?
|
# 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]
|
# Options: [true, false]
|
||||||
# Default: true
|
# Default: true
|
||||||
|
|
|
@ -25,7 +25,6 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
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/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
|
@ -67,6 +66,11 @@ import (
|
||||||
// description: not found
|
// description: not found
|
||||||
// '406':
|
// '406':
|
||||||
// description: not acceptable
|
// 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':
|
// '500':
|
||||||
// description: internal server error
|
// description: internal server error
|
||||||
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
|
@ -87,7 +91,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
return
|
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)
|
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -101,7 +105,25 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
form.IP = signUpIP
|
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 {
|
if errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
|
@ -109,40 +131,3 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
|
|
||||||
apiutil.JSON(c, http.StatusOK, ti)
|
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,
|
"domain": null,
|
||||||
"created_at": "2022-06-04T13:12:00.000Z",
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
"email": "tortle.dude@example.org",
|
"email": "tortle.dude@example.org",
|
||||||
"ip": "118.44.18.196",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -249,7 +249,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-05-17T13:10:59.000Z",
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
"email": "admin@example.org",
|
"email": "admin@example.org",
|
||||||
"ip": "89.122.255.1",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -295,7 +295,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-05-17T13:10:59.000Z",
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
"email": "admin@example.org",
|
"email": "admin@example.org",
|
||||||
"ip": "89.122.255.1",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -354,7 +354,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-06-04T13:12:00.000Z",
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
"email": "tortle.dude@example.org",
|
"email": "tortle.dude@example.org",
|
||||||
"ip": "118.44.18.196",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -576,7 +576,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-06-04T13:12:00.000Z",
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
"email": "tortle.dude@example.org",
|
"email": "tortle.dude@example.org",
|
||||||
"ip": "118.44.18.196",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -798,7 +798,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-06-04T13:12:00.000Z",
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
"email": "tortle.dude@example.org",
|
"email": "tortle.dude@example.org",
|
||||||
"ip": "118.44.18.196",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
|
|
@ -50,8 +50,8 @@ type AdminAccountInfo struct {
|
||||||
// The locale of the account. (ISO 639 Part 1 two-letter language code)
|
// The locale of the account. (ISO 639 Part 1 two-letter language code)
|
||||||
// example: en
|
// example: en
|
||||||
Locale string `json:"locale"`
|
Locale string `json:"locale"`
|
||||||
// The reason given when requesting an invite.
|
// The reason given when signing up.
|
||||||
// Null if not known / remote account.
|
// Null if no reason / remote account.
|
||||||
// example: Pleaaaaaaaaaaaaaaase!!
|
// example: Pleaaaaaaaaaaaaaaase!!
|
||||||
InviteRequest *string `json:"invite_request"`
|
InviteRequest *string `json:"invite_request"`
|
||||||
// The current role of the account.
|
// The current role of the account.
|
||||||
|
|
|
@ -26,13 +26,14 @@ type Notification struct {
|
||||||
// The id of the notification in the database.
|
// The id of the notification in the database.
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
// The type of event that resulted in the notification.
|
// The type of event that resulted in the notification.
|
||||||
// follow = Someone followed you
|
// follow = Someone followed you. `account` will be set.
|
||||||
// follow_request = Someone requested to follow you
|
// follow_request = Someone requested to follow you. `account` will be set.
|
||||||
// mention = Someone mentioned you in their status
|
// mention = Someone mentioned you in their status. `status` will be set. `account` will be set.
|
||||||
// reblog = Someone boosted one of your statuses
|
// reblog = Someone boosted one of your statuses. `status` will be set. `account` will be set.
|
||||||
// favourite = Someone favourited one of your statuses
|
// 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
|
// 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 = 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"`
|
Type string `json:"type"`
|
||||||
// The timestamp of the notification (ISO 8601 Datetime)
|
// The timestamp of the notification (ISO 8601 Datetime)
|
||||||
CreatedAt string `json:"created_at"`
|
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,
|
AccountID: exampleID,
|
||||||
CreatedAt: exampleTime,
|
CreatedAt: exampleTime,
|
||||||
UpdatedAt: exampleTime,
|
UpdatedAt: exampleTime,
|
||||||
Reason: exampleText,
|
|
||||||
Privacy: gtsmodel.VisibilityFollowersOnly,
|
Privacy: gtsmodel.VisibilityFollowersOnly,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
Language: "fr",
|
Language: "fr",
|
||||||
|
@ -629,11 +628,8 @@ func sizeofUser() uintptr {
|
||||||
Email: exampleURI,
|
Email: exampleURI,
|
||||||
AccountID: exampleID,
|
AccountID: exampleID,
|
||||||
EncryptedPassword: exampleTextSmall,
|
EncryptedPassword: exampleTextSmall,
|
||||||
CurrentSignInAt: exampleTime,
|
|
||||||
LastSignInAt: exampleTime,
|
|
||||||
InviteID: exampleID,
|
InviteID: exampleID,
|
||||||
ChosenLanguages: []string{"en", "fr", "jp"},
|
Reason: exampleText,
|
||||||
FilteredLanguages: []string{"en", "fr", "jp"},
|
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
CreatedByApplicationID: exampleID,
|
CreatedByApplicationID: exampleID,
|
||||||
LastEmailedAt: exampleTime,
|
LastEmailedAt: exampleTime,
|
||||||
|
@ -641,10 +637,10 @@ func sizeofUser() uintptr {
|
||||||
ConfirmationSentAt: exampleTime,
|
ConfirmationSentAt: exampleTime,
|
||||||
ConfirmedAt: exampleTime,
|
ConfirmedAt: exampleTime,
|
||||||
UnconfirmedEmail: exampleURI,
|
UnconfirmedEmail: exampleURI,
|
||||||
Moderator: func() *bool { ok := true; return &ok }(),
|
Moderator: util.Ptr(false),
|
||||||
Admin: func() *bool { ok := true; return &ok }(),
|
Admin: util.Ptr(false),
|
||||||
Disabled: func() *bool { ok := true; return &ok }(),
|
Disabled: util.Ptr(false),
|
||||||
Approved: func() *bool { ok := true; return &ok }(),
|
Approved: util.Ptr(false),
|
||||||
ResetPasswordToken: exampleTextSmall,
|
ResetPasswordToken: exampleTextSmall,
|
||||||
ResetPasswordSentAt: exampleTime,
|
ResetPasswordSentAt: exampleTime,
|
||||||
ExternalID: exampleID,
|
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)."`
|
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."`
|
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?"`
|
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."`
|
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."`
|
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),
|
InstanceLanguages: make(language.Languages, 0),
|
||||||
|
|
||||||
AccountsRegistrationOpen: true,
|
AccountsRegistrationOpen: true,
|
||||||
AccountsApprovalRequired: true,
|
|
||||||
AccountsReasonRequired: true,
|
AccountsReasonRequired: true,
|
||||||
AccountsAllowCustomCSS: false,
|
AccountsAllowCustomCSS: false,
|
||||||
AccountsCustomCSSLength: 10000,
|
AccountsCustomCSSLength: 10000,
|
||||||
|
|
|
@ -93,7 +93,6 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
|
||||||
|
|
||||||
// Accounts
|
// Accounts
|
||||||
cmd.Flags().Bool(AccountsRegistrationOpenFlag(), cfg.AccountsRegistrationOpen, fieldtag("AccountsRegistrationOpen", "usage"))
|
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(AccountsReasonRequiredFlag(), cfg.AccountsReasonRequired, fieldtag("AccountsReasonRequired", "usage"))
|
||||||
cmd.Flags().Bool(AccountsAllowCustomCSSFlag(), cfg.AccountsAllowCustomCSS, fieldtag("AccountsAllowCustomCSS", "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
|
// SetAccountsRegistrationOpen safely sets the value for global configuration 'AccountsRegistrationOpen' field
|
||||||
func SetAccountsRegistrationOpen(v bool) { global.SetAccountsRegistrationOpen(v) }
|
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
|
// GetAccountsReasonRequired safely fetches the Configuration value for state's 'AccountsReasonRequired' field
|
||||||
func (st *ConfigState) GetAccountsReasonRequired() (v bool) {
|
func (st *ConfigState) GetAccountsReasonRequired() (v bool) {
|
||||||
st.mutex.RLock()
|
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 returns one account with the given ID, or an error if something goes wrong.
|
||||||
GetAccountByID(ctx context.Context, id string) (*gtsmodel.Account, error)
|
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 returns one account with the given URI, or an error if something goes wrong.
|
||||||
GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
GetAccountByURI(ctx context.Context, uri string) (*gtsmodel.Account, error)
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
@ -36,7 +37,7 @@ type Admin interface {
|
||||||
// C) something went wrong in the db
|
// C) something went wrong in the db
|
||||||
IsEmailAvailable(ctx context.Context, email string) (bool, error)
|
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!
|
// 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)
|
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
|
// This is needed for things like serving instance information through /api/v1/instance
|
||||||
CreateInstanceInstance(ctx context.Context) error
|
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
|
ACTION FUNCS
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
@ -121,7 +122,6 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
|
||||||
|
|
||||||
settings := >smodel.AccountSettings{
|
settings := >smodel.AccountSettings{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Reason: newSignup.Reason,
|
|
||||||
Privacy: gtsmodel.VisibilityDefault,
|
Privacy: gtsmodel.VisibilityDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +197,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (
|
||||||
Account: account,
|
Account: account,
|
||||||
EncryptedPassword: string(encryptedPassword),
|
EncryptedPassword: string(encryptedPassword),
|
||||||
SignUpIP: newSignup.SignUpIP.To4(),
|
SignUpIP: newSignup.SignUpIP.To4(),
|
||||||
|
Reason: newSignup.Reason,
|
||||||
Locale: newSignup.Locale,
|
Locale: newSignup.Locale,
|
||||||
UnconfirmedEmail: newSignup.Email,
|
UnconfirmedEmail: newSignup.Email,
|
||||||
CreatedByApplicationID: newSignup.AppID,
|
CreatedByApplicationID: newSignup.AppID,
|
||||||
|
@ -331,6 +332,113 @@ func (a *adminDB) CreateInstanceInstance(ctx context.Context) error {
|
||||||
return nil
|
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
|
ACTION FUNCS
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -380,3 +380,33 @@ func (i *instanceDB) GetInstanceModeratorAddresses(ctx context.Context) ([]strin
|
||||||
|
|
||||||
return addresses, nil
|
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"
|
"context"
|
||||||
|
|
||||||
oldgtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20230328203024_migration_fix"
|
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/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"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 {
|
for _, n := range notif {
|
||||||
if n.OriginAccountID == originAccount.ID || n.TargetAccountID == targetAccount.ID {
|
if n.OriginAccountID == originAccount.ID && n.TargetAccountID == targetAccount.ID {
|
||||||
suite.FailNowf(
|
suite.FailNowf(
|
||||||
"",
|
"",
|
||||||
"no notifications with origin account id %s and target account %s should remain",
|
"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
|
// GetInstanceModeratorAddresses returns a slice of email addresses belonging to active
|
||||||
// (as in, not suspended) moderators + admins on this instance.
|
// (as in, not suspended) moderators + admins on this instance.
|
||||||
GetInstanceModeratorAddresses(ctx context.Context) ([]string, error)
|
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.sender.SendConfirmEmail("user@example.org", confirmData)
|
||||||
suite.Len(suite.sentEmails, 1)
|
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() {
|
func (suite *EmailTestSuite) TestTemplateReset() {
|
||||||
|
|
|
@ -68,6 +68,10 @@ func (s *noopSender) SendReportClosedEmail(toAddress string, data ReportClosedDa
|
||||||
return s.sendTemplate(reportClosedTemplate, reportClosedSubject, data, toAddress)
|
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 {
|
func (s *noopSender) sendTemplate(template string, subject string, data any, toAddresses ...string) error {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
if err := s.template.ExecuteTemplate(buf, template, data); err != nil {
|
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
|
// 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.
|
// know that a report that they created has been closed / resolved by an admin.
|
||||||
SendReportClosedEmail(toAddress string, data ReportClosedData) error
|
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.
|
// 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.
|
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.
|
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.
|
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
|
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?
|
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?
|
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
|
NotificationFave NotificationType = "favourite" // NotificationFave -- someone faved/liked one of your statuses
|
||||||
NotificationPoll NotificationType = "poll" // NotificationPoll -- a poll you voted in or created has ended
|
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.
|
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"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account.
|
// User represents one signed-up user of this GoToSocial instance.
|
||||||
// To cross reference this local user with their account (which can be local or remote), use the AccountID field.
|
//
|
||||||
|
// 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 {
|
type User struct {
|
||||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
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
|
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.
|
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.
|
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.
|
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?
|
SignUpIP net.IP `bun:",nullzero"` // IP this user used to sign up. Only stored for pending sign-ups.
|
||||||
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?
|
|
||||||
InviteID string `bun:"type:CHAR(26),nullzero"` // id of the user who invited this user (who let this joker in?)
|
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?
|
Reason string `bun:",nullzero"` // What reason was given for signing up when this user was created?
|
||||||
FilteredLanguages []string `bun:",nullzero"` // What languages does this user not want to see?
|
|
||||||
Locale string `bun:",nullzero"` // In what timezone/locale is this user located?
|
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
|
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.
|
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)
|
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
|
// NewSignup models parameters for the creation
|
||||||
// of a new user + account on this instance.
|
// of a new user + account on this instance.
|
||||||
//
|
//
|
||||||
// Aside from username, email, and password, it is
|
// Aside from username, email, and password, it is
|
||||||
// fine to use zero values on fields of this struct.
|
// 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 {
|
type NewSignup struct {
|
||||||
Username string // Username of the new account.
|
Username string // Username of the new account (required).
|
||||||
Email string // Email address of the user.
|
Email string // Email address of the user (required).
|
||||||
Password string // Plaintext (not yet hashed) password for the user.
|
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).
|
Reason string // Reason given by the user when submitting a sign up request (optional).
|
||||||
PreApproved bool // Mark the new user/account as preapproved (optional)
|
PreApproved bool // Mark the new user/account as preapproved (optional)
|
||||||
|
|
|
@ -20,6 +20,7 @@ package account
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -32,15 +33,48 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create processes the given form for creating a new account,
|
// 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(
|
func (p *Processor) Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
appToken oauth2.TokenInfo,
|
|
||||||
app *gtsmodel.Application,
|
app *gtsmodel.Application,
|
||||||
form *apimodel.AccountCreateRequest,
|
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)
|
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("db error checking email availability: %w", err)
|
err := fmt.Errorf("db error checking email availability: %w", err)
|
||||||
|
@ -67,38 +101,61 @@ func (p *Processor) Create(
|
||||||
reason = form.Reason
|
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{
|
user, err := p.state.DB.NewSignup(ctx, gtsmodel.NewSignup{
|
||||||
Username: form.Username,
|
Username: form.Username,
|
||||||
Email: form.Email,
|
Email: form.Email,
|
||||||
Password: form.Password,
|
Password: form.Password,
|
||||||
Reason: text.SanitizeToPlaintext(reason),
|
Reason: text.SanitizeToPlaintext(reason),
|
||||||
PreApproved: !config.GetAccountsApprovalRequired(), // Mark as approved if no approval required.
|
SignUpIP: form.IP,
|
||||||
SignUpIP: form.IP,
|
Locale: form.Locale,
|
||||||
Locale: form.Locale,
|
AppID: app.ID,
|
||||||
AppID: app.ID,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("db error creating new signup: %w", err)
|
err := fmt.Errorf("db error creating new signup: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(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
|
// There are side effects for creating a new account
|
||||||
// (confirmation emails etc), perform these async.
|
// (confirmation emails etc), perform these async.
|
||||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ObjectProfile,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: user.Account,
|
GTSModel: user,
|
||||||
OriginAccount: user.Account,
|
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{
|
return &apimodel.Token{
|
||||||
AccessToken: accessToken.GetAccess(),
|
AccessToken: accessToken.GetAccess(),
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
|
|
|
@ -569,11 +569,6 @@ func stubbifyUser(user *gtsmodel.User) ([]string, error) {
|
||||||
|
|
||||||
user.EncryptedPassword = string(dummyPassword)
|
user.EncryptedPassword = string(dummyPassword)
|
||||||
user.SignUpIP = net.IPv4zero
|
user.SignUpIP = net.IPv4zero
|
||||||
user.CurrentSignInAt = never
|
|
||||||
user.CurrentSignInIP = net.IPv4zero
|
|
||||||
user.LastSignInAt = never
|
|
||||||
user.LastSignInIP = net.IPv4zero
|
|
||||||
user.SignInCount = 1
|
|
||||||
user.Locale = ""
|
user.Locale = ""
|
||||||
user.CreatedByApplicationID = ""
|
user.CreatedByApplicationID = ""
|
||||||
user.LastEmailedAt = never
|
user.LastEmailedAt = never
|
||||||
|
@ -585,11 +580,6 @@ func stubbifyUser(user *gtsmodel.User) ([]string, error) {
|
||||||
return []string{
|
return []string{
|
||||||
"encrypted_password",
|
"encrypted_password",
|
||||||
"sign_up_ip",
|
"sign_up_ip",
|
||||||
"current_sign_in_at",
|
|
||||||
"current_sign_in_ip",
|
|
||||||
"last_sign_in_at",
|
|
||||||
"last_sign_in_ip",
|
|
||||||
"sign_in_count",
|
|
||||||
"locale",
|
"locale",
|
||||||
"created_by_application_id",
|
"created_by_application_id",
|
||||||
"last_emailed_at",
|
"last_emailed_at",
|
||||||
|
|
|
@ -78,11 +78,6 @@ func (suite *AccountDeleteTestSuite) TestAccountDeleteLocal() {
|
||||||
suite.WithinDuration(time.Now(), updatedUser.UpdatedAt, 1*time.Minute)
|
suite.WithinDuration(time.Now(), updatedUser.UpdatedAt, 1*time.Minute)
|
||||||
suite.NotEqual(updatedUser.EncryptedPassword, ogUser.EncryptedPassword)
|
suite.NotEqual(updatedUser.EncryptedPassword, ogUser.EncryptedPassword)
|
||||||
suite.Equal(net.IPv4zero, updatedUser.SignUpIP)
|
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.Locale)
|
||||||
suite.Zero(updatedUser.CreatedByApplicationID)
|
suite.Zero(updatedUser.CreatedByApplicationID)
|
||||||
suite.Zero(updatedUser.LastEmailedAt)
|
suite.Zero(updatedUser.LastEmailedAt)
|
||||||
|
|
|
@ -60,31 +60,14 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ma
|
||||||
prevMinIDValue = n.ID
|
prevMinIDValue = n.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure this notification should be shown to requester.
|
visible, err := p.notifVisible(ctx, n, authed.Account)
|
||||||
if n.OriginAccount != nil {
|
if err != nil {
|
||||||
// Account is set, ensure it's visible to notif target.
|
log.Debugf(ctx, "skipping notification %s because of an error checking notification visibility: %v", n.ID, err)
|
||||||
visible, err := p.filter.AccountVisible(ctx, authed.Account, n.OriginAccount)
|
continue
|
||||||
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 n.Status != nil {
|
if !visible {
|
||||||
// Status is set, ensure it's visible to notif target.
|
continue
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := p.converter.NotificationToAPINotification(ctx, n)
|
item, err := p.converter.NotificationToAPINotification(ctx, n)
|
||||||
|
@ -142,3 +125,44 @@ func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth)
|
||||||
|
|
||||||
return nil
|
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"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
var oneWeek = 168 * time.Hour
|
// EmailGetUserForConfirmToken retrieves the user (with account) from
|
||||||
|
// the database for the given "confirm your email" token string.
|
||||||
// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link
|
func (p *Processor) EmailGetUserForConfirmToken(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||||
// in a 'confirm your email address' type email.
|
|
||||||
func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
|
||||||
if token == "" {
|
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)
|
user, err := p.state.DB.GetUserByConfirmationToken(ctx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == db.ErrNoEntries {
|
if !errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
// 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 {
|
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 {
|
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() {
|
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 {
|
return user, nil
|
||||||
// no pending email confirmations so just return OK
|
}
|
||||||
|
|
||||||
|
// 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
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure token not expired.
|
||||||
|
const oneWeek = 168 * time.Hour
|
||||||
if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) {
|
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
|
// Mark the user's email address as confirmed,
|
||||||
updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"}
|
// and remove the unconfirmed address and the token.
|
||||||
user.Email = user.UnconfirmedEmail
|
user.Email = user.UnconfirmedEmail
|
||||||
user.UnconfirmedEmail = ""
|
user.UnconfirmedEmail = ""
|
||||||
user.ConfirmedAt = time.Now()
|
user.ConfirmedAt = time.Now()
|
||||||
user.ConfirmationToken = ""
|
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)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,7 @@ func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() {
|
||||||
// confirm with the token set above
|
// confirm with the token set above
|
||||||
updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
|
updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
|
||||||
suite.Nil(updatedUser)
|
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) {
|
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 {
|
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 {
|
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.
|
// Notify mods of the new signup.
|
||||||
user, err := p.state.DB.GetUserByAccountID(ctx, account.ID)
|
if err := p.surface.notifySignup(ctx, newUser); err != nil {
|
||||||
if err != nil {
|
log.Errorf(ctx, "error notifying mods of new sign-up: %v", err)
|
||||||
return gtserror.Newf("db error getting user for account id %s: %w", account.ID, 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)
|
log.Errorf(ctx, "error emailing confirm: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -458,7 +463,7 @@ func (p *clientAPI) UpdateReport(ctx context.Context, cMsg messages.FromClientAP
|
||||||
return nil
|
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)
|
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)
|
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:
|
// TODO: handle additional side effects of flag creation:
|
||||||
// - notify admins by dm / notification
|
// - 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)
|
log.Errorf(ctx, "error emailing report opened: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,41 +31,9 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *surface) emailReportOpened(ctx context.Context, report *gtsmodel.Report) error {
|
// emailUserReportClosed emails the user who created the
|
||||||
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
|
// given report, to inform them the report has been closed.
|
||||||
if err != nil {
|
func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Report) error {
|
||||||
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 {
|
|
||||||
user, err := s.state.DB.GetUserByAccountID(ctx, report.Account.ID)
|
user, err := s.state.DB.GetUserByAccountID(ctx, report.Account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("db error getting user: %w", err)
|
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)
|
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 == "" ||
|
if user.UnconfirmedEmail == "" ||
|
||||||
user.UnconfirmedEmail == user.Email {
|
user.UnconfirmedEmail == user.Email {
|
||||||
// User has already confirmed this
|
// 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(
|
if err := s.emailSender.SendConfirmEmail(
|
||||||
user.UnconfirmedEmail,
|
user.UnconfirmedEmail,
|
||||||
email.ConfirmData{
|
email.ConfirmData{
|
||||||
Username: username,
|
Username: user.Account.Username,
|
||||||
InstanceURL: instance.URI,
|
InstanceURL: instance.URI,
|
||||||
InstanceName: instance.Title,
|
InstanceName: instance.Title,
|
||||||
ConfirmLink: confirmLink,
|
ConfirmLink: confirmLink,
|
||||||
|
@ -158,3 +128,77 @@ func (s *surface) emailPleaseConfirm(ctx context.Context, user *gtsmodel.User, u
|
||||||
|
|
||||||
return nil
|
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()
|
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
|
// notify creates, inserts, and streams a new
|
||||||
// notification to the target account if it
|
// notification to the target account if it
|
||||||
// doesn't yet exist with the given parameters.
|
// 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
|
// targets into this function without filtering
|
||||||
// for non-local first.
|
// for non-local first.
|
||||||
//
|
//
|
||||||
// targetAccountID and originAccountID must be
|
// targetAccount and originAccount must be
|
||||||
// set, but statusID can be an empty string.
|
// set, but statusID can be an empty string.
|
||||||
func (s *surface) notify(
|
func (s *surface) notify(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
|
@ -29,11 +29,8 @@ type User struct {
|
||||||
Email string `json:"email,omitempty" bun:",nullzero"`
|
Email string `json:"email,omitempty" bun:",nullzero"`
|
||||||
AccountID string `json:"accountID" bun:",nullzero"`
|
AccountID string `json:"accountID" bun:",nullzero"`
|
||||||
EncryptedPassword string `json:"encryptedPassword" bun:",nullzero"`
|
EncryptedPassword string `json:"encryptedPassword" bun:",nullzero"`
|
||||||
CurrentSignInAt *time.Time `json:"currentSignInAt,omitempty" bun:",nullzero"`
|
Reason string `json:"reason" bun:",nullzero"`
|
||||||
LastSignInAt *time.Time `json:"lastSignInAt,omitempty" bun:",nullzero"`
|
|
||||||
InviteID string `json:"inviteID,omitempty" 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"`
|
Locale string `json:"locale" bun:",nullzero"`
|
||||||
LastEmailedAt time.Time `json:"lastEmailedAt,omitempty" bun:",nullzero"`
|
LastEmailedAt time.Time `json:"lastEmailedAt,omitempty" bun:",nullzero"`
|
||||||
ConfirmationToken string `json:"confirmationToken,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
|
email = user.UnconfirmedEmail
|
||||||
}
|
}
|
||||||
|
|
||||||
if i := user.CurrentSignInIP.String(); i != "<nil>" {
|
if i := user.SignUpIP.String(); i != "<nil>" {
|
||||||
ip = &i
|
ip = &i
|
||||||
}
|
}
|
||||||
|
|
||||||
locale = user.Locale
|
locale = user.Locale
|
||||||
if a.Settings.Reason != "" {
|
if user.Reason != "" {
|
||||||
inviteRequest = &a.Settings.Reason
|
inviteRequest = &user.Reason
|
||||||
}
|
}
|
||||||
|
|
||||||
if *user.Admin {
|
if *user.Admin {
|
||||||
|
@ -1003,7 +1003,7 @@ func (c *Converter) InstanceToAPIV1Instance(ctx context.Context, i *gtsmodel.Ins
|
||||||
Version: config.GetSoftwareVersion(),
|
Version: config.GetSoftwareVersion(),
|
||||||
Languages: config.GetInstanceLanguages().TagStrs(),
|
Languages: config.GetInstanceLanguages().TagStrs(),
|
||||||
Registrations: config.GetAccountsRegistrationOpen(),
|
Registrations: config.GetAccountsRegistrationOpen(),
|
||||||
ApprovalRequired: config.GetAccountsApprovalRequired(),
|
ApprovalRequired: true, // approval always required
|
||||||
InvitesEnabled: false, // todo: not supported yet
|
InvitesEnabled: false, // todo: not supported yet
|
||||||
MaxTootChars: uint(config.GetStatusesMaxChars()),
|
MaxTootChars: uint(config.GetStatusesMaxChars()),
|
||||||
Rules: c.InstanceRulesToAPIRules(i.Rules),
|
Rules: c.InstanceRulesToAPIRules(i.Rules),
|
||||||
|
@ -1172,8 +1172,8 @@ func (c *Converter) InstanceToAPIV2Instance(ctx context.Context, i *gtsmodel.Ins
|
||||||
|
|
||||||
// registrations
|
// registrations
|
||||||
instance.Registrations.Enabled = config.GetAccountsRegistrationOpen()
|
instance.Registrations.Enabled = config.GetAccountsRegistrationOpen()
|
||||||
instance.Registrations.ApprovalRequired = config.GetAccountsApprovalRequired()
|
instance.Registrations.ApprovalRequired = true // always required
|
||||||
instance.Registrations.Message = nil // todo: not implemented
|
instance.Registrations.Message = nil // todo: not implemented
|
||||||
|
|
||||||
// contact
|
// contact
|
||||||
instance.Contact.Email = i.ContactEmail
|
instance.Contact.Email = i.ContactEmail
|
||||||
|
|
|
@ -1386,7 +1386,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-06-04T13:12:00.000Z",
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
"email": "tortle.dude@example.org",
|
"email": "tortle.dude@example.org",
|
||||||
"ip": "118.44.18.196",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -1443,7 +1443,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-05-17T13:10:59.000Z",
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
"email": "admin@example.org",
|
"email": "admin@example.org",
|
||||||
"ip": "89.122.255.1",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -1489,7 +1489,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-05-17T13:10:59.000Z",
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
"email": "admin@example.org",
|
"email": "admin@example.org",
|
||||||
"ip": "89.122.255.1",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -1558,7 +1558,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-06-04T13:12:00.000Z",
|
"created_at": "2022-06-04T13:12:00.000Z",
|
||||||
"email": "tortle.dude@example.org",
|
"email": "tortle.dude@example.org",
|
||||||
"ip": "118.44.18.196",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -1880,7 +1880,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-05-17T13:10:59.000Z",
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
"email": "admin@example.org",
|
"email": "admin@example.org",
|
||||||
"ip": "89.122.255.1",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
@ -1926,7 +1926,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
||||||
"domain": null,
|
"domain": null,
|
||||||
"created_at": "2022-05-17T13:10:59.000Z",
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
"email": "admin@example.org",
|
"email": "admin@example.org",
|
||||||
"ip": "89.122.255.1",
|
"ip": null,
|
||||||
"ips": [],
|
"ips": [],
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"invite_request": null,
|
"invite_request": null,
|
||||||
|
|
|
@ -348,3 +348,42 @@ func FilterContexts(contexts []apimodel.FilterContext) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
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)
|
user, errWithCode := m.processor.User().EmailConfirm(c.Request.Context(), token)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
apiutil.WebErrorHandler(c, errWithCode, instanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve page informing user that their
|
||||||
|
// email address is now confirmed.
|
||||||
page := apiutil.WebPage{
|
page := apiutil.WebPage{
|
||||||
Template: "confirmed.tmpl",
|
Template: "confirmed_email.tmpl",
|
||||||
Instance: instance,
|
Instance: instance,
|
||||||
Extra: map[string]any{
|
Extra: map[string]any{
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
"username": user.Account.Username,
|
"username": user.Account.Username,
|
||||||
|
"token": token,
|
||||||
|
"approved": *user.Approved,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,7 @@ Disallow: /oauth/
|
||||||
Disallow: /check_your_email
|
Disallow: /check_your_email
|
||||||
Disallow: /wait_for_approval
|
Disallow: /wait_for_approval
|
||||||
Disallow: /account_disabled
|
Disallow: /account_disabled
|
||||||
|
Disallow: /signup
|
||||||
|
|
||||||
# Well-known endpoints.
|
# Well-known endpoints.
|
||||||
Disallow: /.well-known/
|
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"
|
settingsPanelGlob = settingsPathPrefix + "/*panel"
|
||||||
userPanelPath = settingsPathPrefix + "/user"
|
userPanelPath = settingsPathPrefix + "/user"
|
||||||
adminPanelPath = settingsPathPrefix + "/admin"
|
adminPanelPath = settingsPathPrefix + "/admin"
|
||||||
|
signupPath = "/signup"
|
||||||
|
|
||||||
cacheControlHeader = "Cache-Control" // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
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
|
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, customCSSPath, m.customCSSGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler)
|
r.AttachHandler(http.MethodGet, rssFeedPath, m.rssFeedGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, confirmEmailPath, m.confirmEmailGETHandler)
|
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, robotsPath, m.robotsGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, aboutPath, m.aboutGETHandler)
|
r.AttachHandler(http.MethodGet, aboutPath, m.aboutGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler)
|
r.AttachHandler(http.MethodGet, domainBlockListPath, m.domainBlockListGETHandler)
|
||||||
r.AttachHandler(http.MethodGet, tagsPath, m.tagGETHandler)
|
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
|
// 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) })
|
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",
|
"account-domain": "peepee",
|
||||||
"accounts-allow-custom-css": true,
|
"accounts-allow-custom-css": true,
|
||||||
"accounts-approval-required": false,
|
|
||||||
"accounts-custom-css-length": 5000,
|
"accounts-custom-css-length": 5000,
|
||||||
"accounts-reason-required": false,
|
"accounts-reason-required": false,
|
||||||
"accounts-registration-open": true,
|
"accounts-registration-open": true,
|
||||||
|
@ -224,7 +223,6 @@ GTS_INSTANCE_LANGUAGES="nl,en-gb" \
|
||||||
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=true \
|
GTS_ACCOUNTS_ALLOW_CUSTOM_CSS=true \
|
||||||
GTS_ACCOUNTS_CUSTOM_CSS_LENGTH=5000 \
|
GTS_ACCOUNTS_CUSTOM_CSS_LENGTH=5000 \
|
||||||
GTS_ACCOUNTS_REGISTRATION_OPEN=true \
|
GTS_ACCOUNTS_REGISTRATION_OPEN=true \
|
||||||
GTS_ACCOUNTS_APPROVAL_REQUIRED=false \
|
|
||||||
GTS_ACCOUNTS_REASON_REQUIRED=false \
|
GTS_ACCOUNTS_REASON_REQUIRED=false \
|
||||||
GTS_MEDIA_IMAGE_MAX_SIZE=420 \
|
GTS_MEDIA_IMAGE_MAX_SIZE=420 \
|
||||||
GTS_MEDIA_VIDEO_MAX_SIZE=420 \
|
GTS_MEDIA_VIDEO_MAX_SIZE=420 \
|
||||||
|
|
|
@ -89,7 +89,6 @@ var testDefaults = config.Configuration{
|
||||||
},
|
},
|
||||||
|
|
||||||
AccountsRegistrationOpen: true,
|
AccountsRegistrationOpen: true,
|
||||||
AccountsApprovalRequired: true,
|
|
||||||
AccountsReasonRequired: true,
|
AccountsReasonRequired: true,
|
||||||
AccountsAllowCustomCSS: true,
|
AccountsAllowCustomCSS: true,
|
||||||
AccountsCustomCSSLength: 10000,
|
AccountsCustomCSSLength: 10000,
|
||||||
|
|
|
@ -20,6 +20,7 @@ package testrig
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"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.
|
// 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) {
|
sendCallback = func(toAddress string, message string) {
|
||||||
sentEmails[toAddress] = message
|
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)
|
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.
|
// NewTestClients returns a map of Clients keyed according to which account they are used by.
|
||||||
func NewTestClients() map[string]*gtsmodel.Client {
|
func NewTestClients() map[string]*gtsmodel.Client {
|
||||||
clients := 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": {
|
"admin_account": {
|
||||||
ID: "01F8MGWSJCND9BWBD4WGJXBM93",
|
ID: "01F8MGWSJCND9BWBD4WGJXBM93",
|
||||||
Secret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a",
|
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.
|
// NewTestApplications returns a map of applications keyed to which number application they are.
|
||||||
func NewTestApplications() map[string]*gtsmodel.Application {
|
func NewTestApplications() map[string]*gtsmodel.Application {
|
||||||
apps := 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": {
|
"admin_account": {
|
||||||
ID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
ID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
Name: "superseriousbusiness",
|
Name: "superseriousbusiness",
|
||||||
|
@ -167,14 +182,8 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
||||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||||
SignUpIP: net.ParseIP("199.222.111.89"),
|
SignUpIP: net.ParseIP("199.222.111.89"),
|
||||||
UpdatedAt: time.Time{},
|
UpdatedAt: time.Time{},
|
||||||
CurrentSignInAt: time.Time{},
|
|
||||||
CurrentSignInIP: nil,
|
|
||||||
LastSignInAt: time.Time{},
|
|
||||||
LastSignInIP: nil,
|
|
||||||
SignInCount: 0,
|
|
||||||
InviteID: "",
|
InviteID: "",
|
||||||
ChosenLanguages: []string{},
|
Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
|
||||||
FilteredLanguages: []string{},
|
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
LastEmailedAt: time.Time{},
|
LastEmailedAt: time.Time{},
|
||||||
|
@ -195,16 +204,9 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
||||||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
||||||
CreatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
CreatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
||||||
SignUpIP: net.ParseIP("89.22.189.19"),
|
SignUpIP: nil,
|
||||||
UpdatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
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: "",
|
InviteID: "",
|
||||||
ChosenLanguages: []string{"en"},
|
|
||||||
FilteredLanguages: []string{},
|
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
CreatedByApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
CreatedByApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
LastEmailedAt: TimeMustParse("2022-06-03T13:12:00Z"),
|
LastEmailedAt: TimeMustParse("2022-06-03T13:12:00Z"),
|
||||||
|
@ -225,16 +227,10 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
||||||
CreatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
CreatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
||||||
SignUpIP: net.ParseIP("59.99.19.172"),
|
SignUpIP: nil,
|
||||||
UpdatedAt: TimeMustParse("2022-06-01T13:12:00Z"),
|
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: "",
|
InviteID: "",
|
||||||
ChosenLanguages: []string{"en"},
|
Reason: "I wanna be on this damned webbed site so bad! Please! Wow",
|
||||||
FilteredLanguages: []string{},
|
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
LastEmailedAt: TimeMustParse("2022-06-02T13:12:00Z"),
|
LastEmailedAt: TimeMustParse("2022-06-02T13:12:00Z"),
|
||||||
|
@ -255,16 +251,9 @@ func NewTestUsers() map[string]*gtsmodel.User {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
EncryptedPassword: "$2y$10$ggWz5QWwnx6kzb9g0tnIJurFtE0dhr5Zfeaqs9iFuUIXzafQlJVZS", // 'password'
|
||||||
CreatedAt: TimeMustParse("2022-05-23T13:12:00Z"),
|
CreatedAt: TimeMustParse("2022-05-23T13:12:00Z"),
|
||||||
SignUpIP: net.ParseIP("59.99.19.172"),
|
SignUpIP: nil,
|
||||||
UpdatedAt: TimeMustParse("2022-05-23T13:12:00Z"),
|
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: "",
|
InviteID: "",
|
||||||
ChosenLanguages: []string{"en"},
|
|
||||||
FilteredLanguages: []string{},
|
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
CreatedByApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||||
LastEmailedAt: TimeMustParse("2022-06-06T13:12:00Z"),
|
LastEmailedAt: TimeMustParse("2022-06-06T13:12:00Z"),
|
||||||
|
@ -664,7 +653,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
AccountID: "01F8MH0BBE4FHXPH513MBVFHB0",
|
AccountID: "01F8MH0BBE4FHXPH513MBVFHB0",
|
||||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||||
UpdatedAt: 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,
|
Privacy: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
Language: "en",
|
Language: "en",
|
||||||
|
@ -675,7 +663,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
|
CreatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
|
||||||
UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
|
UpdatedAt: TimeMustParse("2022-05-17T13:10:59Z"),
|
||||||
Reason: "",
|
|
||||||
Privacy: gtsmodel.VisibilityPublic,
|
Privacy: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
Language: "en",
|
Language: "en",
|
||||||
|
@ -686,7 +673,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
|
CreatedAt: TimeMustParse("2022-05-20T11:09:18Z"),
|
||||||
UpdatedAt: 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,
|
Privacy: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
Language: "en",
|
Language: "en",
|
||||||
|
@ -697,7 +683,6 @@ func NewTestAccountSettings() map[string]*gtsmodel.AccountSettings {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||||
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||||
Reason: "",
|
|
||||||
Privacy: gtsmodel.VisibilityFollowersOnly,
|
Privacy: gtsmodel.VisibilityFollowersOnly,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
Language: "fr",
|
Language: "fr",
|
||||||
|
@ -2428,6 +2413,15 @@ func NewTestNotifications() map[string]*gtsmodel.Notification {
|
||||||
StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R",
|
StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||||
Read: util.Ptr(false),
|
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 *****
|
***** SECTION 4: SHAMEFUL MESS *****
|
||||||
************************************/
|
************************************/
|
||||||
|
@ -419,33 +470,8 @@ pre, pre[class*="language-"] {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Below section stylings are used
|
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 {
|
section.error {
|
||||||
word-break: break-word;
|
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
|
TODO: list and blocklist are only used
|
||||||
in settings panel and on blocklist page;
|
in settings panel and on blocklist page;
|
||||||
|
|
|
@ -59,37 +59,26 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- 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" -}}
|
{{- 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 -}}
|
{{- 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 -}}
|
{{- 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 -}}
|
{{- end -}}
|
||||||
|
on account profiles.
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "statusLimits" -}}
|
{{- define "statusLimits" -}}
|
||||||
Statuses can contain up to
|
Statuses can contain up to
|
||||||
{{- .instance.Configuration.Statuses.MaxCharacters }} characters, and
|
<b>{{- .instance.Configuration.Statuses.MaxCharacters }} characters</b>, and
|
||||||
{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments.
|
<b>{{- .instance.Configuration.Statuses.MaxMediaAttachments }} media attachments</b>.
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "pollLimits" -}}
|
{{- define "pollLimits" -}}
|
||||||
Polls can have up to
|
Polls can have up to
|
||||||
{{- .instance.Configuration.Polls.MaxOptions }} options, with
|
<b>{{- .instance.Configuration.Polls.MaxOptions }} options</b>, with
|
||||||
{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option.
|
<b>{{- .instance.Configuration.Polls.MaxCharactersPerOption }} characters per option</b>.
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- with . }}
|
{{- with . }}
|
||||||
|
@ -102,6 +91,7 @@ Polls can have up to
|
||||||
<li><a href="#contact">Contact</a></li>
|
<li><a href="#contact">Contact</a></li>
|
||||||
<li><a href="#features">Features</a></li>
|
<li><a href="#features">Features</a></li>
|
||||||
<li><a href="#languages">Languages</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="#rules">Rules</a></li>
|
||||||
<li><a href="#terms">Terms and Conditions</a></li>
|
<li><a href="#terms">Terms and Conditions</a></li>
|
||||||
<li><a href="#moderated-servers">Moderated Servers</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>
|
<h3 id="features">Instance Features</h3>
|
||||||
<div class="about-section-contents">
|
<div class="about-section-contents">
|
||||||
<ul>
|
<ul>
|
||||||
<li>{{- template "registrationLimits" . -}}</li>
|
|
||||||
<li>{{- template "customCSSLimits" . -}}</li>
|
|
||||||
<li>{{- template "statusLimits" . -}}</li>
|
<li>{{- template "statusLimits" . -}}</li>
|
||||||
<li>{{- template "pollLimits" . -}}</li>
|
<li>{{- template "pollLimits" . -}}</li>
|
||||||
|
<li>{{- template "customCSSLimits" . -}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -160,6 +149,7 @@ Polls can have up to
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{{- include "index_register.tmpl" . | indent 1 }}
|
||||||
<section class="about-section" role="region" aria-labelledby="rules">
|
<section class="about-section" role="region" aria-labelledby="rules">
|
||||||
<h3 id="rules">Instance Rules</h3>
|
<h3 id="rules">Instance Rules</h3>
|
||||||
<div class="about-section-contents">
|
<div class="about-section-contents">
|
||||||
|
|
|
@ -19,22 +19,25 @@
|
||||||
|
|
||||||
{{- with . }}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<form action="/oauth/authorize" method="POST">
|
<section class="with-form" aria-labelledby="authorize">
|
||||||
<h1>Hi {{ .user -}}!</h1>
|
<h2 id="authorize">Authorize app</h2>
|
||||||
<p>
|
<form action="/oauth/authorize" method="POST">
|
||||||
Application
|
<p>Hi <b>{{- .user -}}</b>!</p>
|
||||||
{{- if .appwebsite }}
|
<p>
|
||||||
<a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
|
Application
|
||||||
{{- else }}
|
{{- if .appwebsite }}
|
||||||
<b>{{- .appname -}}</b>
|
<a href="{{- .appwebsite -}}" rel="nofollow noreferrer noopener" target="_blank">{{- .appname -}}</a>
|
||||||
{{- end }}
|
{{- else }}
|
||||||
would like to perform actions on your behalf, with scope
|
<b>{{- .appname -}}</b>
|
||||||
<em>{{- .scope -}}</em>.
|
{{- end }}
|
||||||
</p>
|
would like to perform actions on your behalf, with scope
|
||||||
<p>
|
<em>{{- .scope -}}</em>.
|
||||||
To continue, the application will redirect to: <code>{{- .redirect -}}</code>
|
</p>
|
||||||
</p>
|
<p>
|
||||||
<button type="submit" style="width:200px;">Allow</button>
|
To continue, the application will redirect to: <code>{{- .redirect -}}</code>
|
||||||
</form>
|
</p>
|
||||||
|
<button type="submit" class="btn btn-success">Allow</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{- end }}
|
{{- 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 . }}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section>
|
<section aria-labelledby="confirmed">
|
||||||
<h1>Email Address Confirmed</h1>
|
<h2 id="confirmed">Email address confirmed</h2>
|
||||||
<p>Thanks {{ .username -}}! Your email address <b>{{- .email -}}</b> has been confirmed.<p>
|
<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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -17,12 +17,14 @@
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// 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 . }}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<form action="/oauth/finalize" method="POST">
|
<section class="with-form" aria-labelledby="finalize">
|
||||||
<h1>Hi {{ .name -}}!</h1>
|
<h2 id="finalize">Finalize sign-in to {{ .instance.Title -}}</h2>
|
||||||
<p>
|
<form action="/oauth/finalize" method="POST">
|
||||||
You are about to sign-up to {{ .instance.Title -}}.
|
<p>
|
||||||
To ensure the best experience for you, we need you to provide some additional details.
|
Hi <b>{{- .name -}}</b>!
|
||||||
</p>
|
</p>
|
||||||
<div class="callout">
|
<p>
|
||||||
<p class="callout-title">Important</p>
|
You are about to create an account on <b>{{- .instance.Title -}}</b>.
|
||||||
<p>Due to the way the ActivityPub standard works, you <strong>cannot</strong> change your username after it has been set.</p>
|
To finish the process, you must select your username.
|
||||||
</div>
|
</p>
|
||||||
<div class="labelinput">
|
<div class="labelinput">
|
||||||
<label for="username">Username <small>(must contain only lowercase letters, numbers, and underscores)</small></label>
|
<label for="username">
|
||||||
<input
|
Username (lowercase a-z, numbers, and underscores; max 64 characters).<br/>
|
||||||
type="text"
|
<small>Your username will be part of your fediverse handle, and cannot be changed later, so choose thoughtfully!</small>
|
||||||
class="form-control"
|
</label>
|
||||||
name="username"
|
<input
|
||||||
required
|
id="username"
|
||||||
placeholder="Please enter your desired username"
|
type="text"
|
||||||
value="{{- .preferredUsername -}}"
|
name="username"
|
||||||
>
|
required
|
||||||
</div>
|
placeholder="Please enter your desired username"
|
||||||
<input type="hidden" name="name" value="{{- .name -}}">
|
pattern="^[a-z0-9_]{1,64}$"
|
||||||
<button type="submit" style="width: 100%; margin-top: 1rem;" class="btn btn-success">Submit</button>
|
title="lowercase a-z, numbers, and underscores; max 64 characters"
|
||||||
</form>
|
value="{{- .preferredUsername -}}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="name" value="{{- .name -}}">
|
||||||
|
<button type="submit" class="btn btn-success">Submit</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -35,6 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{- include "index_what_is_this.tmpl" . | indent 1 }}
|
{{- include "index_what_is_this.tmpl" . | indent 1 }}
|
||||||
|
{{- include "index_register.tmpl" . | indent 1 }}
|
||||||
{{- include "index_apps.tmpl" . | indent 1 }}
|
{{- include "index_apps.tmpl" . | indent 1 }}
|
||||||
</main>
|
</main>
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -22,8 +22,9 @@
|
||||||
<h3 id="apps">Client applications</h3>
|
<h3 id="apps">Client applications</h3>
|
||||||
<div class="about-section-contents">
|
<div class="about-section-contents">
|
||||||
<p>
|
<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.
|
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>
|
</p>
|
||||||
<ul class="applist nodot" role="group">
|
<ul class="applist nodot" role="group">
|
||||||
<li class="applist-entry">
|
<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>
|
<p>
|
||||||
You can join the fediverse by running your own instance of an ActivityPub software,
|
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,
|
or by finding an existing instance that aligns with your values and expectations,
|
||||||
and registering an account there.
|
and registering an account.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
To help you find an instance that suits you, you can try one of the following tools:
|
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://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>
|
<li><a href="https://fedidb.org/network" rel="nofollow noreferrer noopener" target="_blank">FediDB (opens in a new tab)</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
{{- if .instance.Registrations }}
|
||||||
|
<p>Or, just <a href="#signup">register for an account on this instance</a>!</p>
|
||||||
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{{- end }}
|
{{- end }}
|
|
@ -20,7 +20,7 @@
|
||||||
{{- with . }}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section class="oob-token">
|
<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>
|
<p>Here's your out-of-band token with scope "<em>{{- .scope -}}</em>", use it wisely:</p>
|
||||||
<code>{{- .oobToken -}}</code>
|
<code>{{- .oobToken -}}</code>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -19,16 +19,16 @@
|
||||||
|
|
||||||
{{- with . }}
|
{{- with . }}
|
||||||
<main>
|
<main>
|
||||||
<section class="sign-in" aria-labelledby="sign-in">
|
<section class="with-form" aria-labelledby="sign-in">
|
||||||
<h2 id="sign-in">Sign in</h2>
|
<h2 id="sign-in">Sign in</h2>
|
||||||
<form action="/auth/sign_in" method="POST">
|
<form action="/auth/sign_in" method="POST">
|
||||||
<div class="labelinput">
|
<div class="labelinput">
|
||||||
<label for="email">Email</label>
|
<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>
|
||||||
<div class="labelinput">
|
<div class="labelinput">
|
||||||
<label for="password">Password</label>
|
<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>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">Sign in</button>
|
<button type="submit" class="btn btn-success">Sign in</button>
|
||||||
</form>
|
</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