forked from FoundKeyGang/FoundKey
feat: アカウント作成にメールアドレス必須にするオプション (#7856)
* feat: アカウント作成にメールアドレス必須にするオプション
* ui
* fix bug
* fix bug
* fix bug
* 🎨
This commit is contained in:
parent
e568c3888f
commit
b875cc9949
22 changed files with 356 additions and 37 deletions
|
@ -11,6 +11,7 @@
|
|||
## 12.x.x (unreleased)
|
||||
|
||||
### Improvements
|
||||
- アカウント登録にメールアドレスの設定を必須にするオプション
|
||||
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
|
||||
- クライアント: MFM関数構文のサジェストを実装
|
||||
- ActivityPub: HTML -> MFMの変換を強化
|
||||
|
|
|
@ -791,6 +791,12 @@ resolved: "解決済み"
|
|||
unresolved: "未解決"
|
||||
itsOn: "オンになっています"
|
||||
itsOff: "オフになっています"
|
||||
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
||||
|
||||
_signup:
|
||||
almostThere: "ほとんど完了です"
|
||||
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。"
|
||||
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
|
||||
|
||||
_accountDelete:
|
||||
accountDelete: "アカウントの削除"
|
||||
|
|
14
migration/1633068642000-email-required-for-signup.ts
Normal file
14
migration/1633068642000-email-required-for-signup.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class emailRequiredForSignup1633068642000 implements MigrationInterface {
|
||||
name = 'emailRequiredForSignup1633068642000'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "emailRequiredForSignup" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "emailRequiredForSignup"`);
|
||||
}
|
||||
|
||||
}
|
16
migration/1633071909016-user-pending.ts
Normal file
16
migration/1633071909016-user-pending.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class userPending1633071909016 implements MigrationInterface {
|
||||
name = 'userPending1633071909016'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "user_pending" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "code" character varying(128) NOT NULL, "username" character varying(128) NOT NULL, "email" character varying(128) NOT NULL, "password" character varying(128) NOT NULL, CONSTRAINT "PK_d4c84e013c98ec02d19b8fbbafa" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4e5c4c99175638ec0761714ab0" ON "user_pending" ("code") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_4e5c4c99175638ec0761714ab0"`);
|
||||
await queryRunner.query(`DROP TABLE "user_pending"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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', {
|
||||
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);
|
||||
login(res.i);
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
this.submitting = false;
|
||||
this.$refs.hcaptcha?.reset?.();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
50
src/client/pages/signup-complete.vue
Normal file
50
src/client/pages/signup-complete.vue
Normal file
|
@ -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>
|
|
@ -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') },
|
||||
|
|
|
@ -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
|
||||
];
|
||||
|
||||
|
|
|
@ -148,6 +148,11 @@ export class Meta {
|
|||
@JoinColumn()
|
||||
public proxyAccount: User | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public emailRequiredForSignup: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
32
src/models/entities/user-pending.ts
Normal file
32
src/models/entities/user-pending.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -11,12 +11,21 @@ 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');
|
||||
}
|
||||
|
||||
if (password != null && passwordHash == null) {
|
||||
// Validate password
|
||||
if (!Users.validatePassword.ok(password)) {
|
||||
throw new Error('INVALID_PASSWORD');
|
||||
|
@ -24,7 +33,8 @@ export async function signup(username: User['username'], password: UserProfile['
|
|||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
hash = await bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
// Generate secret
|
||||
const secret = generateUserToken();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
37
src/server/api/endpoints/email-address/available.ts
Normal file
37
src/server/api/endpoints/email-address/available.ts
Normal file
|
@ -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
|
||||
};
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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());
|
||||
|
|
35
src/server/api/private/signup-pending.ts
Normal file
35
src/server/api/private/signup-pending.ts
Normal file
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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,8 +61,34 @@ export default async (ctx: Koa.Context) => {
|
|||
RegistrationTickets.delete(ticket.id);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
await UserPendings.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
email: emailAddress,
|
||||
username: username,
|
||||
password: hash,
|
||||
});
|
||||
|
||||
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.status = 204;
|
||||
} else {
|
||||
try {
|
||||
const { account, secret } = await signup(username, password, host);
|
||||
const { account, secret } = await signup({
|
||||
username, password, host
|
||||
});
|
||||
|
||||
const res = await Users.pack(account, account, {
|
||||
detail: true,
|
||||
|
@ -62,4 +101,5 @@ export default async (ctx: Koa.Context) => {
|
|||
} catch (e) {
|
||||
ctx.throw(400, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue