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)
|
## 12.x.x (unreleased)
|
||||||
|
|
||||||
### Improvements
|
### Improvements
|
||||||
|
- アカウント登録にメールアドレスの設定を必須にするオプション
|
||||||
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
|
- クライアント: アニメーションを減らす設定をメニューのアニメーションにも適用するように
|
||||||
- クライアント: MFM関数構文のサジェストを実装
|
- クライアント: MFM関数構文のサジェストを実装
|
||||||
- ActivityPub: HTML -> MFMの変換を強化
|
- ActivityPub: HTML -> MFMの変換を強化
|
||||||
|
|
|
@ -791,6 +791,12 @@ resolved: "解決済み"
|
||||||
unresolved: "未解決"
|
unresolved: "未解決"
|
||||||
itsOn: "オンになっています"
|
itsOn: "オンになっています"
|
||||||
itsOff: "オフになっています"
|
itsOff: "オフになっています"
|
||||||
|
emailRequiredForSignup: "アカウント登録にメールアドレスを必須にする"
|
||||||
|
|
||||||
|
_signup:
|
||||||
|
almostThere: "ほとんど完了です"
|
||||||
|
emailAddressInfo: "あなたが使っているメールアドレスを入力してください。"
|
||||||
|
emailSent: "入力されたメールアドレス({email})宛に確認のメールが送信されました。メールに記載されたリンクにアクセスすると、アカウントの作成が完了します。"
|
||||||
|
|
||||||
_accountDelete:
|
_accountDelete:
|
||||||
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="_monolithic_">
|
||||||
<div class="_section">
|
<div class="_section">
|
||||||
<XSignup :auto-set="autoSet" @signup="onSignup"/>
|
<XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</XModalWindow>
|
</XModalWindow>
|
||||||
|
@ -40,6 +40,10 @@ export default defineComponent({
|
||||||
onSignup(res) {
|
onSignup(res) {
|
||||||
this.$emit('done', res);
|
this.$emit('done', res);
|
||||||
this.$refs.dialog.close();
|
this.$refs.dialog.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
onSignupEmailPending() {
|
||||||
|
this.$refs.dialog.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,13 +10,23 @@
|
||||||
<template #prefix>@</template>
|
<template #prefix>@</template>
|
||||||
<template #suffix>@{{ host }}</template>
|
<template #suffix>@{{ host }}</template>
|
||||||
<template #caption>
|
<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 === '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-else-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-else-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-else-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-else-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-else-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-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>
|
</template>
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkInput class="_formBlock" v-model="password" type="password" :autocomplete="Math.random()" required @update:modelValue="onChangePassword" data-cy-signup-password>
|
<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: '',
|
password: '',
|
||||||
retypedPassword: '',
|
retypedPassword: '',
|
||||||
invitationCode: '',
|
invitationCode: '',
|
||||||
|
email: '',
|
||||||
url,
|
url,
|
||||||
usernameState: null,
|
usernameState: null,
|
||||||
|
emailState: null,
|
||||||
passwordStrength: '',
|
passwordStrength: '',
|
||||||
passwordRetypeState: null,
|
passwordRetypeState: null,
|
||||||
submitting: false,
|
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() {
|
onChangePassword() {
|
||||||
if (this.password == '') {
|
if (this.password == '') {
|
||||||
this.passwordStrength = '';
|
this.passwordStrength = '';
|
||||||
|
@ -174,20 +203,30 @@ export default defineComponent({
|
||||||
os.api('signup', {
|
os.api('signup', {
|
||||||
username: this.username,
|
username: this.username,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
|
emailAddress: this.email,
|
||||||
invitationCode: this.invitationCode,
|
invitationCode: this.invitationCode,
|
||||||
'hcaptcha-response': this.hCaptchaResponse,
|
'hcaptcha-response': this.hCaptchaResponse,
|
||||||
'g-recaptcha-response': this.reCaptchaResponse,
|
'g-recaptcha-response': this.reCaptchaResponse,
|
||||||
}).then(() => {
|
}).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,
|
username: this.username,
|
||||||
password: this.password
|
password: this.password
|
||||||
}).then(res => {
|
}).then(res => {
|
||||||
this.$emit('signup', res);
|
this.$emit('signup', res);
|
||||||
|
|
||||||
if (this.autoSet) {
|
if (this.autoSet) {
|
||||||
return login(res.i);
|
login(res.i);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.submitting = false;
|
this.submitting = false;
|
||||||
this.$refs.hcaptcha?.reset?.();
|
this.$refs.hcaptcha?.reset?.();
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
|
|
||||||
<FormSwitch v-model="enableRegistration">{{ $ts.enableRegistration }}</FormSwitch>
|
<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>
|
<FormButton @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||||
</FormSuspense>
|
</FormSuspense>
|
||||||
</FormBase>
|
</FormBase>
|
||||||
|
@ -50,6 +52,7 @@ export default defineComponent({
|
||||||
enableHcaptcha: false,
|
enableHcaptcha: false,
|
||||||
enableRecaptcha: false,
|
enableRecaptcha: false,
|
||||||
enableRegistration: false,
|
enableRegistration: false,
|
||||||
|
emailRequiredForSignup: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -63,11 +66,13 @@ export default defineComponent({
|
||||||
this.enableHcaptcha = meta.enableHcaptcha;
|
this.enableHcaptcha = meta.enableHcaptcha;
|
||||||
this.enableRecaptcha = meta.enableRecaptcha;
|
this.enableRecaptcha = meta.enableRecaptcha;
|
||||||
this.enableRegistration = !meta.disableRegistration;
|
this.enableRegistration = !meta.disableRegistration;
|
||||||
|
this.emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||||
},
|
},
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
disableRegistration: !this.enableRegistration,
|
disableRegistration: !this.enableRegistration,
|
||||||
|
emailRequiredForSignup: this.emailRequiredForSignup,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
fetchInstance();
|
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: '/@: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: '/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: '/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: '/announcements', component: page('announcements') },
|
||||||
{ path: '/about', component: page('about') },
|
{ path: '/about', component: page('about') },
|
||||||
{ path: '/about-misskey', component: page('about-misskey') },
|
{ 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 { RegistryItem } from '@/models/entities/registry-item';
|
||||||
import { Ad } from '@/models/entities/ad';
|
import { Ad } from '@/models/entities/ad';
|
||||||
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
|
import { PasswordResetRequest } from '@/models/entities/password-reset-request';
|
||||||
|
import { UserPending } from '@/models/entities/user-pending';
|
||||||
|
|
||||||
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
|
||||||
|
|
||||||
|
@ -173,6 +174,7 @@ export const entities = [
|
||||||
RegistryItem,
|
RegistryItem,
|
||||||
Ad,
|
Ad,
|
||||||
PasswordResetRequest,
|
PasswordResetRequest,
|
||||||
|
UserPending,
|
||||||
...charts as any
|
...charts as any
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -148,6 +148,11 @@ export class Meta {
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
public proxyAccount: User | null;
|
public proxyAccount: User | null;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
public emailRequiredForSignup: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
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 { RegistryItem } from './entities/registry-item';
|
||||||
import { Ad } from './entities/ad';
|
import { Ad } from './entities/ad';
|
||||||
import { PasswordResetRequest } from './entities/password-reset-request';
|
import { PasswordResetRequest } from './entities/password-reset-request';
|
||||||
|
import { UserPending } from './entities/user-pending';
|
||||||
|
|
||||||
export const Announcements = getRepository(Announcement);
|
export const Announcements = getRepository(Announcement);
|
||||||
export const AnnouncementReads = getRepository(AnnouncementRead);
|
export const AnnouncementReads = getRepository(AnnouncementRead);
|
||||||
|
@ -76,6 +77,7 @@ export const PollVotes = getRepository(PollVote);
|
||||||
export const Users = getCustomRepository(UserRepository);
|
export const Users = getCustomRepository(UserRepository);
|
||||||
export const UserProfiles = getRepository(UserProfile);
|
export const UserProfiles = getRepository(UserProfile);
|
||||||
export const UserKeypairs = getRepository(UserKeypair);
|
export const UserKeypairs = getRepository(UserKeypair);
|
||||||
|
export const UserPendings = getRepository(UserPending);
|
||||||
export const AttestationChallenges = getRepository(AttestationChallenge);
|
export const AttestationChallenges = getRepository(AttestationChallenge);
|
||||||
export const UserSecurityKeys = getRepository(UserSecurityKey);
|
export const UserSecurityKeys = getRepository(UserSecurityKey);
|
||||||
export const UserPublickeys = getRepository(UserPublickey);
|
export const UserPublickeys = getRepository(UserPublickey);
|
||||||
|
|
|
@ -11,12 +11,21 @@ import { UserKeypair } from '@/models/entities/user-keypair';
|
||||||
import { usersChart } from '@/services/chart/index';
|
import { usersChart } from '@/services/chart/index';
|
||||||
import { UsedUsername } from '@/models/entities/used-username';
|
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
|
// Validate username
|
||||||
if (!Users.validateLocalUsername.ok(username)) {
|
if (!Users.validateLocalUsername.ok(username)) {
|
||||||
throw new Error('INVALID_USERNAME');
|
throw new Error('INVALID_USERNAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (password != null && passwordHash == null) {
|
||||||
// Validate password
|
// Validate password
|
||||||
if (!Users.validatePassword.ok(password)) {
|
if (!Users.validatePassword.ok(password)) {
|
||||||
throw new Error('INVALID_PASSWORD');
|
throw new Error('INVALID_PASSWORD');
|
||||||
|
@ -24,7 +33,8 @@ export async function signup(username: User['username'], password: UserProfile['
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
const salt = await bcrypt.genSalt(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
const hash = await bcrypt.hash(password, salt);
|
hash = await bcrypt.hash(password, salt);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate secret
|
// Generate secret
|
||||||
const secret = generateUserToken();
|
const secret = generateUserToken();
|
||||||
|
|
|
@ -35,7 +35,10 @@ export default define(meta, async (ps, _me) => {
|
||||||
})) === 0;
|
})) === 0;
|
||||||
if (!noUsers && !me?.isAdmin) throw new Error('access denied');
|
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, {
|
const res = await Users.pack(account, account, {
|
||||||
detail: true,
|
detail: true,
|
||||||
|
|
|
@ -93,6 +93,10 @@ export const meta = {
|
||||||
validator: $.optional.bool,
|
validator: $.optional.bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emailRequiredForSignup: {
|
||||||
|
validator: $.optional.bool,
|
||||||
|
},
|
||||||
|
|
||||||
enableHcaptcha: {
|
enableHcaptcha: {
|
||||||
validator: $.optional.bool,
|
validator: $.optional.bool,
|
||||||
},
|
},
|
||||||
|
@ -374,6 +378,10 @@ export default define(meta, async (ps, me) => {
|
||||||
set.proxyRemoteFiles = ps.proxyRemoteFiles;
|
set.proxyRemoteFiles = ps.proxyRemoteFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.emailRequiredForSignup !== undefined) {
|
||||||
|
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.enableHcaptcha !== undefined) {
|
if (ps.enableHcaptcha !== undefined) {
|
||||||
set.enableHcaptcha = ps.enableHcaptcha;
|
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,
|
type: 'boolean' as const,
|
||||||
optional: false as const, nullable: false as const
|
optional: false as const, nullable: false as const
|
||||||
},
|
},
|
||||||
|
emailRequiredForSignup: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
optional: false as const, nullable: false as const
|
||||||
|
},
|
||||||
enableHcaptcha: {
|
enableHcaptcha: {
|
||||||
type: 'boolean' as const,
|
type: 'boolean' as const,
|
||||||
optional: false as const, nullable: false as const
|
optional: false as const, nullable: false as const
|
||||||
|
@ -488,6 +492,7 @@ export default define(meta, async (ps, me) => {
|
||||||
disableGlobalTimeline: instance.disableGlobalTimeline,
|
disableGlobalTimeline: instance.disableGlobalTimeline,
|
||||||
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
|
||||||
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
|
||||||
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
enableHcaptcha: instance.enableHcaptcha,
|
enableHcaptcha: instance.enableHcaptcha,
|
||||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||||
enableRecaptcha: instance.enableRecaptcha,
|
enableRecaptcha: instance.enableRecaptcha,
|
||||||
|
@ -537,6 +542,7 @@ export default define(meta, async (ps, me) => {
|
||||||
registration: !instance.disableRegistration,
|
registration: !instance.disableRegistration,
|
||||||
localTimeLine: !instance.disableLocalTimeline,
|
localTimeLine: !instance.disableLocalTimeline,
|
||||||
globalTimeLine: !instance.disableGlobalTimeline,
|
globalTimeLine: !instance.disableGlobalTimeline,
|
||||||
|
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||||
elasticsearch: config.elasticsearch ? true : false,
|
elasticsearch: config.elasticsearch ? true : false,
|
||||||
hcaptcha: instance.enableHcaptcha,
|
hcaptcha: instance.enableHcaptcha,
|
||||||
recaptcha: instance.enableRecaptcha,
|
recaptcha: instance.enableRecaptcha,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import endpoints from './endpoints';
|
||||||
import handler from './api-handler';
|
import handler from './api-handler';
|
||||||
import signup from './private/signup';
|
import signup from './private/signup';
|
||||||
import signin from './private/signin';
|
import signin from './private/signin';
|
||||||
|
import signupPending from './private/signup-pending';
|
||||||
import discord from './service/discord';
|
import discord from './service/discord';
|
||||||
import github from './service/github';
|
import github from './service/github';
|
||||||
import twitter from './service/twitter';
|
import twitter from './service/twitter';
|
||||||
|
@ -65,6 +66,7 @@ for (const endpoint of endpoints) {
|
||||||
|
|
||||||
router.post('/signup', signup);
|
router.post('/signup', signup);
|
||||||
router.post('/signin', signin);
|
router.post('/signin', signin);
|
||||||
|
router.post('/signup-pending', signupPending);
|
||||||
|
|
||||||
router.use(discord.routes());
|
router.use(discord.routes());
|
||||||
router.use(github.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 * as Koa from 'koa';
|
||||||
|
import rndstr from 'rndstr';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
import { fetchMeta } from '@/misc/fetch-meta';
|
import { fetchMeta } from '@/misc/fetch-meta';
|
||||||
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha';
|
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 { 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) => {
|
export default async (ctx: Koa.Context) => {
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
|
@ -29,8 +34,16 @@ export default async (ctx: Koa.Context) => {
|
||||||
const password = body['password'];
|
const password = body['password'];
|
||||||
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
|
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] || null) : null;
|
||||||
const invitationCode = body['invitationCode'];
|
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') {
|
if (invitationCode == null || typeof invitationCode != 'string') {
|
||||||
ctx.status = 400;
|
ctx.status = 400;
|
||||||
return;
|
return;
|
||||||
|
@ -48,8 +61,34 @@ export default async (ctx: Koa.Context) => {
|
||||||
RegistrationTickets.delete(ticket.id);
|
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 {
|
try {
|
||||||
const { account, secret } = await signup(username, password, host);
|
const { account, secret } = await signup({
|
||||||
|
username, password, host
|
||||||
|
});
|
||||||
|
|
||||||
const res = await Users.pack(account, account, {
|
const res = await Users.pack(account, account, {
|
||||||
detail: true,
|
detail: true,
|
||||||
|
@ -62,4 +101,5 @@ export default async (ctx: Koa.Context) => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.throw(400, e);
|
ctx.throw(400, e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -68,6 +68,7 @@ const nodeinfo2 = async () => {
|
||||||
disableRegistration: meta.disableRegistration,
|
disableRegistration: meta.disableRegistration,
|
||||||
disableLocalTimeline: meta.disableLocalTimeline,
|
disableLocalTimeline: meta.disableLocalTimeline,
|
||||||
disableGlobalTimeline: meta.disableGlobalTimeline,
|
disableGlobalTimeline: meta.disableGlobalTimeline,
|
||||||
|
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||||
enableHcaptcha: meta.enableHcaptcha,
|
enableHcaptcha: meta.enableHcaptcha,
|
||||||
enableRecaptcha: meta.enableRecaptcha,
|
enableRecaptcha: meta.enableRecaptcha,
|
||||||
maxNoteTextLength: meta.maxNoteTextLength,
|
maxNoteTextLength: meta.maxNoteTextLength,
|
||||||
|
|
Loading…
Reference in a new issue