diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-10-08 13:37:02 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-10-08 13:37:02 +0900 |
| commit | b875cc994968bb334dfb9d83707e56ab3971a0d1 (patch) | |
| tree | 5655892af829ecad9f040624f8b6cd31410284f9 /src | |
| parent | update dependencies (diff) | |
| download | sharkey-b875cc994968bb334dfb9d83707e56ab3971a0d1.tar.gz sharkey-b875cc994968bb334dfb9d83707e56ab3971a0d1.tar.bz2 sharkey-b875cc994968bb334dfb9d83707e56ab3971a0d1.zip | |
feat: アカウント作成にメールアドレス必須にするオプション (#7856)
* feat: アカウント作成にメールアドレス必須にするオプション
* ui
* fix bug
* fix bug
* fix bug
* :art:
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/components/signup-dialog.vue | 6 | ||||
| -rw-r--r-- | src/client/components/signup.vue | 71 | ||||
| -rw-r--r-- | src/client/pages/instance/security.vue | 5 | ||||
| -rw-r--r-- | src/client/pages/signup-complete.vue | 50 | ||||
| -rw-r--r-- | src/client/router.ts | 1 | ||||
| -rw-r--r-- | src/db/postgre.ts | 2 | ||||
| -rw-r--r-- | src/models/entities/meta.ts | 5 | ||||
| -rw-r--r-- | src/models/entities/user-pending.ts | 32 | ||||
| -rw-r--r-- | src/models/index.ts | 2 | ||||
| -rw-r--r-- | src/server/api/common/signup.ts | 26 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/accounts/create.ts | 5 | ||||
| -rw-r--r-- | src/server/api/endpoints/admin/update-meta.ts | 8 | ||||
| -rw-r--r-- | src/server/api/endpoints/email-address/available.ts | 37 | ||||
| -rw-r--r-- | src/server/api/endpoints/meta.ts | 6 | ||||
| -rw-r--r-- | src/server/api/index.ts | 2 | ||||
| -rw-r--r-- | src/server/api/private/signup-pending.ts | 35 | ||||
| -rw-r--r-- | src/server/api/private/signup.ts | 62 | ||||
| -rw-r--r-- | src/server/nodeinfo.ts | 1 |
18 files changed, 319 insertions, 37 deletions
diff --git a/src/client/components/signup-dialog.vue b/src/client/components/signup-dialog.vue index df1a525055..9741e8c73b 100644 --- a/src/client/components/signup-dialog.vue +++ b/src/client/components/signup-dialog.vue @@ -9,7 +9,7 @@ <div class="_monolithic_"> <div class="_section"> - <XSignup :auto-set="autoSet" @signup="onSignup"/> + <XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/> </div> </div> </XModalWindow> @@ -40,6 +40,10 @@ export default defineComponent({ onSignup(res) { this.$emit('done', res); this.$refs.dialog.close(); + }, + + onSignupEmailPending() { + this.$refs.dialog.close(); } } }); diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue index f555c1df6d..b420bca5a3 100644 --- a/src/client/components/signup.vue +++ b/src/client/components/signup.vue @@ -10,13 +10,23 @@ <template #prefix>@</template> <template #suffix>@{{ host }}</template> <template #caption> - <span v-if="usernameState == 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span> - <span v-if="usernameState == 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span> - <span v-if="usernameState == 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span> - <span v-if="usernameState == 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> - <span v-if="usernameState == 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span> - <span v-if="usernameState == 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span> - <span v-if="usernameState == 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span> + <span v-if="usernameState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span> + <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span> + <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span> + <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> + <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.usernameInvalidFormat }}</span> + <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooShort }}</span> + <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.tooLong }}</span> + </template> + </MkInput> + <MkInput v-if="meta.emailRequiredForSignup" class="_formBlock" v-model="email" type="email" :autocomplete="Math.random()" spellcheck="false" required @update:modelValue="onChangeEmail" data-cy-signup-email> + <template #label>{{ $ts.emailAddress }} <div class="_button _help" v-tooltip:dialog="$ts._signup.emailAddressInfo"><i class="far fa-question-circle"></i></div></template> + <template #prefix><i class="fas fa-envelope"></i></template> + <template #caption> + <span v-if="emailState === 'wait'" style="color:#999"><i class="fas fa-spinner fa-pulse fa-fw"></i> {{ $ts.checking }}</span> + <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="fas fa-check fa-fw"></i> {{ $ts.available }}</span> + <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.unavailable }}</span> + <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="fas fa-exclamation-triangle fa-fw"></i> {{ $ts.error }}</span> </template> </MkInput> <MkInput class="_formBlock" v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password> @@ -87,8 +97,10 @@ export default defineComponent({ password: '', retypedPassword: '', invitationCode: '', + email: '', url, usernameState: null, + emailState: null, passwordStrength: '', passwordRetypeState: null, submitting: false, @@ -148,6 +160,23 @@ export default defineComponent({ }); }, + onChangeEmail() { + if (this.email == '') { + this.emailState = null; + return; + } + + this.emailState = 'wait'; + + os.api('email-address/available', { + emailAddress: this.email + }).then(result => { + this.emailState = result.available ? 'ok' : 'unavailable'; + }).catch(err => { + this.emailState = 'error'; + }); + }, + onChangePassword() { if (this.password == '') { this.passwordStrength = ''; @@ -174,20 +203,30 @@ export default defineComponent({ os.api('signup', { username: this.username, password: this.password, + emailAddress: this.email, invitationCode: this.invitationCode, 'hcaptcha-response': this.hCaptchaResponse, 'g-recaptcha-response': this.reCaptchaResponse, }).then(() => { - return os.api('signin', { - username: this.username, - password: this.password - }).then(res => { - this.$emit('signup', res); + if (this.meta.emailRequiredForSignup) { + os.dialog({ + type: 'success', + title: this.$ts._signup.almostThere, + text: this.$t('_signup.emailSent', { email: this.email }), + }); + this.$emit('signupEmailPending'); + } else { + os.api('signin', { + username: this.username, + password: this.password + }).then(res => { + this.$emit('signup', res); - if (this.autoSet) { - return login(res.i); - } - }); + if (this.autoSet) { + login(res.i); + } + }); + } }).catch(() => { this.submitting = false; this.$refs.hcaptcha?.reset?.(); diff --git a/src/client/pages/instance/security.vue b/src/client/pages/instance/security.vue index 53f923643a..2b525261ae 100644 --- a/src/client/pages/instance/security.vue +++ b/src/client/pages/instance/security.vue @@ -10,6 +10,8 @@ <FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch> + <FormSwitch v-model="emailRequiredForSignup">{{ $ts.emailRequiredForSignup }}</FormSwitch> + <FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> </FormSuspense> </FormBase> @@ -50,6 +52,7 @@ export default defineComponent({ enableHcaptcha: false, enableRecaptcha: false, enableRegistration: false, + emailRequiredForSignup: false, } }, @@ -63,11 +66,13 @@ export default defineComponent({ this.enableHcaptcha = meta.enableHcaptcha; this.enableRecaptcha = meta.enableRecaptcha; this.enableRegistration = !meta.disableRegistration; + this.emailRequiredForSignup = meta.emailRequiredForSignup; }, save() { os.apiWithDialog('admin/update-meta', { disableRegistration: !this.enableRegistration, + emailRequiredForSignup: this.emailRequiredForSignup, }).then(() => { fetchInstance(); }); diff --git a/src/client/pages/signup-complete.vue b/src/client/pages/signup-complete.vue new file mode 100644 index 0000000000..dada92031a --- /dev/null +++ b/src/client/pages/signup-complete.vue @@ -0,0 +1,50 @@ +<template> +<div> + {{ $ts.processing }} +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import * as os from '@client/os'; +import * as symbols from '@client/symbols'; +import { login } from '@client/account'; + +export default defineComponent({ + components: { + + }, + + props: { + code: { + type: String, + required: true + } + }, + + data() { + return { + [symbols.PAGE_INFO]: { + title: this.$ts.signup, + icon: 'fas fa-user' + }, + } + }, + + mounted() { + os.apiWithDialog('signup-pending', { + code: this.code, + }).then(res => { + login(res.i, '/'); + }); + }, + + methods: { + + } +}); +</script> + +<style lang="scss" scoped> + +</style> diff --git a/src/client/router.ts b/src/client/router.ts index 573f285c79..56dc948669 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -23,6 +23,7 @@ const defaultRoutes = [ { path: '/@:acct/room', props: true, component: page('room/room') }, { path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) }, { path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) }, + { path: '/signup-complete/:code', component: page('signup-complete'), props: route => ({ code: route.params.code }) }, { path: '/announcements', component: page('announcements') }, { path: '/about', component: page('about') }, { path: '/about-misskey', component: page('about-misskey') }, diff --git a/src/db/postgre.ts b/src/db/postgre.ts index c963242488..8948f22cdc 100644 --- a/src/db/postgre.ts +++ b/src/db/postgre.ts @@ -72,6 +72,7 @@ import { ChannelNotePining } from '@/models/entities/channel-note-pining'; import { RegistryItem } from '@/models/entities/registry-item'; import { Ad } from '@/models/entities/ad'; import { PasswordResetRequest } from '@/models/entities/password-reset-request'; +import { UserPending } from '@/models/entities/user-pending'; const sqlLogger = dbLogger.createSubLogger('sql', 'white', false); @@ -173,6 +174,7 @@ export const entities = [ RegistryItem, Ad, PasswordResetRequest, + UserPending, ...charts as any ]; diff --git a/src/models/entities/meta.ts b/src/models/entities/meta.ts index 6428aacdf1..9a1a87c155 100644 --- a/src/models/entities/meta.ts +++ b/src/models/entities/meta.ts @@ -151,6 +151,11 @@ export class Meta { @Column('boolean', { default: false, }) + public emailRequiredForSignup: boolean; + + @Column('boolean', { + default: false, + }) public enableHcaptcha: boolean; @Column('varchar', { diff --git a/src/models/entities/user-pending.ts b/src/models/entities/user-pending.ts new file mode 100644 index 0000000000..40482af333 --- /dev/null +++ b/src/models/entities/user-pending.ts @@ -0,0 +1,32 @@ +import { PrimaryColumn, Entity, Index, Column } from 'typeorm'; +import { id } from '../id'; + +@Entity() +export class UserPending { + @PrimaryColumn(id()) + public id: string; + + @Column('timestamp with time zone') + public createdAt: Date; + + @Index({ unique: true }) + @Column('varchar', { + length: 128, + }) + public code: string; + + @Column('varchar', { + length: 128, + }) + public username: string; + + @Column('varchar', { + length: 128, + }) + public email: string; + + @Column('varchar', { + length: 128, + }) + public password: string; +} diff --git a/src/models/index.ts b/src/models/index.ts index 9f8bd104e9..059a3d7b87 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -62,6 +62,7 @@ import { ChannelNotePining } from './entities/channel-note-pining'; import { RegistryItem } from './entities/registry-item'; import { Ad } from './entities/ad'; import { PasswordResetRequest } from './entities/password-reset-request'; +import { UserPending } from './entities/user-pending'; export const Announcements = getRepository(Announcement); export const AnnouncementReads = getRepository(AnnouncementRead); @@ -76,6 +77,7 @@ export const PollVotes = getRepository(PollVote); export const Users = getCustomRepository(UserRepository); export const UserProfiles = getRepository(UserProfile); export const UserKeypairs = getRepository(UserKeypair); +export const UserPendings = getRepository(UserPending); export const AttestationChallenges = getRepository(AttestationChallenge); export const UserSecurityKeys = getRepository(UserSecurityKey); export const UserPublickeys = getRepository(UserPublickey); diff --git a/src/server/api/common/signup.ts b/src/server/api/common/signup.ts index eb3aa09c8c..2ba0d8e479 100644 --- a/src/server/api/common/signup.ts +++ b/src/server/api/common/signup.ts @@ -11,20 +11,30 @@ import { UserKeypair } from '@/models/entities/user-keypair'; import { usersChart } from '@/services/chart/index'; import { UsedUsername } from '@/models/entities/used-username'; -export async function signup(username: User['username'], password: UserProfile['password'], host: string | null = null) { +export async function signup(opts: { + username: User['username']; + password?: string | null; + passwordHash?: UserProfile['password'] | null; + host?: string | null; +}) { + const { username, password, passwordHash, host } = opts; + let hash = passwordHash; + // Validate username if (!Users.validateLocalUsername.ok(username)) { throw new Error('INVALID_USERNAME'); } - // Validate password - if (!Users.validatePassword.ok(password)) { - throw new Error('INVALID_PASSWORD'); - } + if (password != null && passwordHash == null) { + // Validate password + if (!Users.validatePassword.ok(password)) { + throw new Error('INVALID_PASSWORD'); + } - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(password, salt); + // Generate hash of password + const salt = await bcrypt.genSalt(8); + hash = await bcrypt.hash(password, salt); + } // Generate secret const secret = generateUserToken(); diff --git a/src/server/api/endpoints/admin/accounts/create.ts b/src/server/api/endpoints/admin/accounts/create.ts index 9691b9c7e3..fa15e84f77 100644 --- a/src/server/api/endpoints/admin/accounts/create.ts +++ b/src/server/api/endpoints/admin/accounts/create.ts @@ -35,7 +35,10 @@ export default define(meta, async (ps, _me) => { })) === 0; if (!noUsers && !me?.isAdmin) throw new Error('access denied'); - const { account, secret } = await signup(ps.username, ps.password); + const { account, secret } = await signup({ + username: ps.username, + password: ps.password, + }); const res = await Users.pack(account, account, { detail: true, diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 46f30fef7d..55447098dc 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -93,6 +93,10 @@ export const meta = { validator: $.optional.bool, }, + emailRequiredForSignup: { + validator: $.optional.bool, + }, + enableHcaptcha: { validator: $.optional.bool, }, @@ -374,6 +378,10 @@ export default define(meta, async (ps, me) => { set.proxyRemoteFiles = ps.proxyRemoteFiles; } + if (ps.emailRequiredForSignup !== undefined) { + set.emailRequiredForSignup = ps.emailRequiredForSignup; + } + if (ps.enableHcaptcha !== undefined) { set.enableHcaptcha = ps.enableHcaptcha; } diff --git a/src/server/api/endpoints/email-address/available.ts b/src/server/api/endpoints/email-address/available.ts new file mode 100644 index 0000000000..65fe6f9178 --- /dev/null +++ b/src/server/api/endpoints/email-address/available.ts @@ -0,0 +1,37 @@ +import $ from 'cafy'; +import define from '../../define'; +import { UserProfiles } from '@/models/index'; + +export const meta = { + tags: ['users'], + + requireCredential: false as const, + + params: { + emailAddress: { + validator: $.str + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + available: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + } + } + } +}; + +export default define(meta, async (ps) => { + const exist = await UserProfiles.count({ + emailVerified: true, + email: ps.emailAddress, + }); + + return { + available: exist === 0 + }; +}); diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index 3f422dff07..ce21556243 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -104,6 +104,10 @@ export const meta = { type: 'boolean' as const, optional: false as const, nullable: false as const }, + emailRequiredForSignup: { + type: 'boolean' as const, + optional: false as const, nullable: false as const + }, enableHcaptcha: { type: 'boolean' as const, optional: false as const, nullable: false as const @@ -488,6 +492,7 @@ export default define(meta, async (ps, me) => { disableGlobalTimeline: instance.disableGlobalTimeline, driveCapacityPerLocalUserMb: instance.localDriveCapacityMb, driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb, + emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, hcaptchaSiteKey: instance.hcaptchaSiteKey, enableRecaptcha: instance.enableRecaptcha, @@ -537,6 +542,7 @@ export default define(meta, async (ps, me) => { registration: !instance.disableRegistration, localTimeLine: !instance.disableLocalTimeline, globalTimeLine: !instance.disableGlobalTimeline, + emailRequiredForSignup: instance.emailRequiredForSignup, elasticsearch: config.elasticsearch ? true : false, hcaptcha: instance.enableHcaptcha, recaptcha: instance.enableRecaptcha, diff --git a/src/server/api/index.ts b/src/server/api/index.ts index db35fdf9e0..82579075eb 100644 --- a/src/server/api/index.ts +++ b/src/server/api/index.ts @@ -12,6 +12,7 @@ import endpoints from './endpoints'; import handler from './api-handler'; import signup from './private/signup'; import signin from './private/signin'; +import signupPending from './private/signup-pending'; import discord from './service/discord'; import github from './service/github'; import twitter from './service/twitter'; @@ -65,6 +66,7 @@ for (const endpoint of endpoints) { router.post('/signup', signup); router.post('/signin', signin); +router.post('/signup-pending', signupPending); router.use(discord.routes()); router.use(github.routes()); diff --git a/src/server/api/private/signup-pending.ts b/src/server/api/private/signup-pending.ts new file mode 100644 index 0000000000..c0638a1cda --- /dev/null +++ b/src/server/api/private/signup-pending.ts @@ -0,0 +1,35 @@ +import * as Koa from 'koa'; +import { Users, UserPendings, UserProfiles } from '@/models/index'; +import { signup } from '../common/signup'; +import signin from '../common/signin'; + +export default async (ctx: Koa.Context) => { + const body = ctx.request.body; + + const code = body['code']; + + try { + const pendingUser = await UserPendings.findOneOrFail({ code }); + + const { account, secret } = await signup({ + username: pendingUser.username, + passwordHash: pendingUser.password, + }); + + UserPendings.delete({ + id: pendingUser.id, + }); + + const profile = await UserProfiles.findOneOrFail(account.id); + + await UserProfiles.update({ userId: profile.userId }, { + email: pendingUser.email, + emailVerified: true, + emailVerifyCode: null, + }); + + signin(ctx, account); + } catch (e) { + ctx.throw(400, e); + } +}; diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts index ef61767f65..93caaea935 100644 --- a/src/server/api/private/signup.ts +++ b/src/server/api/private/signup.ts @@ -1,8 +1,13 @@ import * as Koa from 'koa'; +import rndstr from 'rndstr'; +import * as bcrypt from 'bcryptjs'; import { fetchMeta } from '@/misc/fetch-meta'; import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha'; -import { Users, RegistrationTickets } from '@/models/index'; +import { Users, RegistrationTickets, UserPendings } from '@/models/index'; import { signup } from '../common/signup'; +import config from '@/config'; +import { sendEmail } from '@/services/send-email'; +import { genId } from '@/misc/gen-id'; export default async (ctx: Koa.Context) => { const body = ctx.request.body; @@ -29,8 +34,16 @@ export default async (ctx: Koa.Context) => { const password = body['password']; const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null; const invitationCode = body['invitationCode']; + const emailAddress = body['emailAddress']; - if (instance && instance.disableRegistration) { + if (instance.emailRequiredForSignup) { + if (emailAddress == null || typeof emailAddress != 'string') { + ctx.status = 400; + return; + } + } + + if (instance.disableRegistration) { if (invitationCode == null || typeof invitationCode != 'string') { ctx.status = 400; return; @@ -48,18 +61,45 @@ export default async (ctx: Koa.Context) => { RegistrationTickets.delete(ticket.id); } - try { - const { account, secret } = await signup(username, password, host); + if (instance.emailRequiredForSignup) { + const code = rndstr('a-z0-9', 16); + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); - const res = await Users.pack(account, account, { - detail: true, - includeSecrets: true + await UserPendings.insert({ + id: genId(), + createdAt: new Date(), + code, + email: emailAddress, + username: username, + password: hash, }); - (res as any).token = secret; + const link = `${config.url}/signup-complete/${code}`; + + sendEmail(emailAddress, 'Signup', + `To complete signup, please click this link:<br><a href="${link}">${link}</a>`, + `To complete signup, please click this link: ${link}`); - ctx.body = res; - } catch (e) { - ctx.throw(400, e); + ctx.status = 204; + } else { + try { + const { account, secret } = await signup({ + username, password, host + }); + + const res = await Users.pack(account, account, { + detail: true, + includeSecrets: true + }); + + (res as any).token = secret; + + ctx.body = res; + } catch (e) { + ctx.throw(400, e); + } } }; diff --git a/src/server/nodeinfo.ts b/src/server/nodeinfo.ts index dec2615086..6a864fcc52 100644 --- a/src/server/nodeinfo.ts +++ b/src/server/nodeinfo.ts @@ -68,6 +68,7 @@ const nodeinfo2 = async () => { disableRegistration: meta.disableRegistration, disableLocalTimeline: meta.disableLocalTimeline, disableGlobalTimeline: meta.disableGlobalTimeline, + emailRequiredForSignup: meta.emailRequiredForSignup, enableHcaptcha: meta.enableHcaptcha, enableRecaptcha: meta.enableRecaptcha, maxNoteTextLength: meta.maxNoteTextLength, |