This commit is contained in:
syuilo 2018-08-17 19:17:23 +09:00
parent dcdb57df9d
commit 2c8f962889
9 changed files with 124 additions and 33 deletions

View file

@ -317,6 +317,8 @@ common/views/components/signin.vue:
login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。" login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
common/views/components/signup.vue: common/views/components/signup.vue:
invitation-code: "招待コード"
invitation-info: "招待コードをお持ちでない方は、<a href=\"{}\">管理者</a>までご連絡ください。"
username: "ユーザー名" username: "ユーザー名"
checking: "確認しています..." checking: "確認しています..."
available: "利用できます" available: "利用できます"

View file

@ -1,5 +1,10 @@
<template> <template>
<form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()"> <form class="mk-signup" @submit.prevent="onSubmit" :autocomplete="Math.random()">
<ui-input v-if="meta.disableRegistration" v-model="invitationCode" type="text" :autocomplete="Math.random()" spellcheck="false" required>
<span>%i18n:@invitation-code%</span>
<span slot="prefix">%fa:id-card-alt%</span>
<p slot="text" v-html="'%i18n:@invitation-info%'.replace('{}', meta.maintainer.url)"></p>
</ui-input>
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername"> <ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :autocomplete="Math.random()" spellcheck="false" required @input="onChangeUsername">
<span>%i18n:@username%</span> <span>%i18n:@username%</span>
<span slot="prefix">@</span> <span slot="prefix">@</span>
@ -46,11 +51,13 @@ export default Vue.extend({
username: '', username: '',
password: '', password: '',
retypedPassword: '', retypedPassword: '',
invitationCode: '',
url, url,
recaptchaSitekey, recaptchaSitekey,
usernameState: null, usernameState: null,
passwordStrength: '', passwordStrength: '',
passwordRetypeState: null passwordRetypeState: null,
meta: null
} }
}, },
computed: { computed: {
@ -61,6 +68,11 @@ export default Vue.extend({
this.usernameState != 'max-range'); this.usernameState != 'max-range');
} }
}, },
created() {
(this as any).os.getMeta().then(meta => {
this.meta = meta;
});
},
methods: { methods: {
onChangeUsername() { onChangeUsername() {
if (this.username == '') { if (this.username == '') {
@ -110,6 +122,7 @@ export default Vue.extend({
(this as any).api('signup', { (this as any).api('signup', {
username: this.username, username: this.username,
password: this.password, password: this.password,
invitationCode: this.invitationCode,
'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null 'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
}).then(() => { }).then(() => {
(this as any).api('signin', { (this as any).api('signin', {

View file

@ -7,6 +7,10 @@
<p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p> <p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
<p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p> <p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
</div> </div>
<div>
<button class="ui" @click="invite">%i18n:@invite%</button>
<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
</div>
</div> </div>
</template> </template>
@ -16,13 +20,21 @@ import Vue from "vue";
export default Vue.extend({ export default Vue.extend({
data() { data() {
return { return {
stats: null stats: null,
inviteCode: null
}; };
}, },
created() { created() {
(this as any).api('stats').then(stats => { (this as any).api('stats').then(stats => {
this.stats = stats; this.stats = stats;
}); });
},
methods: {
invite() {
(this as any).api('admin/invite').then(x => {
this.inviteCode = x.code;
});
}
} }
}); });
</script> </script>

View file

@ -11,4 +11,5 @@ export type IMeta = {
usersCount: number; usersCount: number;
originalUsersCount: number; originalUsersCount: number;
}; };
disableRegistration: boolean;
}; };

View file

@ -0,0 +1,12 @@
import * as mongo from 'mongodb';
import db from '../db/mongodb';
const RegistrationTicket = db.get<IRegistrationTicket>('registrationTickets');
RegistrationTicket.createIndex('code', { unique: true });
export default RegistrationTicket;
export interface IRegistrationTicket {
_id: mongo.ObjectID;
createdAt: Date;
code: string;
}

View file

@ -0,0 +1,26 @@
import rndstr from 'rndstr';
import RegistrationTicket from '../../../../models/registration-tickets';
export const meta = {
desc: {
ja: '招待コードを発行します。'
},
requireCredential: true,
requireAdmin: true,
params: {}
};
export default (params: any) => new Promise(async (res, rej) => {
const code = rndstr({ length: 5, chars: '0-9' });
await RegistrationTicket.insert({
createdAt: new Date(),
code: code
});
res({
code: code
});
});

View file

@ -4,43 +4,43 @@ import getParams from '../../get-params';
import User from '../../../../models/user'; import User from '../../../../models/user';
export const meta = { export const meta = {
desc: { desc: {
ja: '指定したユーザーを凍結します。', ja: '指定したユーザーを凍結します。',
en: 'Suspend a user.' en: 'Suspend a user.'
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireAdmin: true,
params: { params: {
userId: $.type(ID).note({ userId: $.type(ID).note({
desc: { desc: {
ja: '対象のユーザーID', ja: '対象のユーザーID',
en: 'The user ID which you want to suspend' en: 'The user ID which you want to suspend'
} }
}), }),
} }
}; };
export default (params: any) => new Promise(async (res, rej) => { export default (params: any) => new Promise(async (res, rej) => {
const [ps, psErr] = getParams(meta, params); const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr); if (psErr) return rej(psErr);
const user = await User.findOne({ const user = await User.findOne({
_id: ps.userId _id: ps.userId
}); });
if (user == null) { if (user == null) {
return rej('user not found'); return rej('user not found');
} }
await User.findOneAndUpdate({ await User.findOneAndUpdate({
_id: user._id _id: user._id
}, { }, {
$set: { $set: {
isSuspended: true isSuspended: true
} }
}); });
res(); res();
}); });

View file

@ -28,6 +28,7 @@ export default () => new Promise(async (res, rej) => {
model: os.cpus()[0].model, model: os.cpus()[0].model,
cores: os.cpus().length cores: os.cpus().length
}, },
broadcasts: meta.broadcasts broadcasts: meta.broadcasts,
disableRegistration: meta.disableRegistration
}); });
}); });

View file

@ -6,6 +6,7 @@ import User, { IUser, validateUsername, validatePassword, pack } from '../../../
import generateUserToken from '../common/generate-native-user-token'; import generateUserToken from '../common/generate-native-user-token';
import config from '../../../config'; import config from '../../../config';
import Meta from '../../../models/meta'; import Meta from '../../../models/meta';
import RegistrationTicket from '../../../models/registration-tickets';
if (config.recaptcha) { if (config.recaptcha) {
recaptcha.init({ recaptcha.init({
@ -29,6 +30,29 @@ export default async (ctx: Koa.Context) => {
const username = body['username']; const username = body['username'];
const password = body['password']; const password = body['password'];
const invitationCode = body['invitationCode'];
const meta = await Meta.findOne({});
if (meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode != 'string') {
ctx.status = 400;
return;
}
const ticket = await RegistrationTicket.findOne({
code: invitationCode
});
if (ticket == null) {
ctx.status = 400;
return;
}
RegistrationTicket.remove({
_id: ticket._id
});
}
// Validate username // Validate username
if (!validateUsername(username)) { if (!validateUsername(username)) {