parent
							
								
									dcdb57df9d
								
							
						
					
					
						commit
						2c8f962889
					
				
					 9 changed files with 124 additions and 33 deletions
				
			
		| 
						 | 
				
			
			@ -317,6 +317,8 @@ common/views/components/signin.vue:
 | 
			
		|||
  login-failed: "ログインできませんでした。ユーザー名とパスワードを確認してください。"
 | 
			
		||||
 | 
			
		||||
common/views/components/signup.vue:
 | 
			
		||||
  invitation-code: "招待コード"
 | 
			
		||||
  invitation-info: "招待コードをお持ちでない方は、<a href=\"{}\">管理者</a>までご連絡ください。"
 | 
			
		||||
  username: "ユーザー名"
 | 
			
		||||
  checking: "確認しています..."
 | 
			
		||||
  available: "利用できます"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,10 @@
 | 
			
		|||
<template>
 | 
			
		||||
<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">
 | 
			
		||||
		<span>%i18n:@username%</span>
 | 
			
		||||
		<span slot="prefix">@</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -46,11 +51,13 @@ export default Vue.extend({
 | 
			
		|||
			username: '',
 | 
			
		||||
			password: '',
 | 
			
		||||
			retypedPassword: '',
 | 
			
		||||
			invitationCode: '',
 | 
			
		||||
			url,
 | 
			
		||||
			recaptchaSitekey,
 | 
			
		||||
			usernameState: null,
 | 
			
		||||
			passwordStrength: '',
 | 
			
		||||
			passwordRetypeState: null
 | 
			
		||||
			passwordRetypeState: null,
 | 
			
		||||
			meta: null
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -61,6 +68,11 @@ export default Vue.extend({
 | 
			
		|||
				this.usernameState != 'max-range');
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		(this as any).os.getMeta().then(meta => {
 | 
			
		||||
			this.meta = meta;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		onChangeUsername() {
 | 
			
		||||
			if (this.username == '') {
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +122,7 @@ export default Vue.extend({
 | 
			
		|||
			(this as any).api('signup', {
 | 
			
		||||
				username: this.username,
 | 
			
		||||
				password: this.password,
 | 
			
		||||
				invitationCode: this.invitationCode,
 | 
			
		||||
				'g-recaptcha-response': recaptchaSitekey != null ? (window as any).grecaptcha.getResponse() : null
 | 
			
		||||
			}).then(() => {
 | 
			
		||||
				(this as any).api('signin', {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,10 @@
 | 
			
		|||
		<p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
 | 
			
		||||
		<p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div>
 | 
			
		||||
		<button class="ui" @click="invite">%i18n:@invite%</button>
 | 
			
		||||
		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -16,13 +20,21 @@ import Vue from "vue";
 | 
			
		|||
export default Vue.extend({
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			stats: null
 | 
			
		||||
			stats: null,
 | 
			
		||||
			inviteCode: null
 | 
			
		||||
		};
 | 
			
		||||
	},
 | 
			
		||||
	created() {
 | 
			
		||||
		(this as any).api('stats').then(stats => {
 | 
			
		||||
			this.stats = stats;
 | 
			
		||||
		});
 | 
			
		||||
	},
 | 
			
		||||
	methods: {
 | 
			
		||||
		invite() {
 | 
			
		||||
			(this as any).api('admin/invite').then(x => {
 | 
			
		||||
				this.inviteCode = x.code;
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,4 +11,5 @@ export type IMeta = {
 | 
			
		|||
		usersCount: number;
 | 
			
		||||
		originalUsersCount: number;
 | 
			
		||||
	};
 | 
			
		||||
	disableRegistration: boolean;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										12
									
								
								src/models/registration-tickets.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/models/registration-tickets.ts
									
										
									
									
									
										Normal 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/server/api/endpoints/admin/invite.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/server/api/endpoints/admin/invite.ts
									
										
									
									
									
										Normal 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
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -4,43 +4,43 @@ import getParams from '../../get-params';
 | 
			
		|||
import User from '../../../../models/user';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
  desc: {
 | 
			
		||||
    ja: '指定したユーザーを凍結します。',
 | 
			
		||||
    en: 'Suspend a user.'
 | 
			
		||||
  },
 | 
			
		||||
	desc: {
 | 
			
		||||
		ja: '指定したユーザーを凍結します。',
 | 
			
		||||
		en: 'Suspend a user.'
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
  requireCredential: true,
 | 
			
		||||
  requireAdmin: true,
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
	requireAdmin: true,
 | 
			
		||||
 | 
			
		||||
  params: {
 | 
			
		||||
    userId: $.type(ID).note({
 | 
			
		||||
      desc: {
 | 
			
		||||
        ja: '対象のユーザーID',
 | 
			
		||||
        en: 'The user ID which you want to suspend'
 | 
			
		||||
      }
 | 
			
		||||
    }),
 | 
			
		||||
  }
 | 
			
		||||
	params: {
 | 
			
		||||
		userId: $.type(ID).note({
 | 
			
		||||
			desc: {
 | 
			
		||||
				ja: '対象のユーザーID',
 | 
			
		||||
				en: 'The user ID which you want to suspend'
 | 
			
		||||
			}
 | 
			
		||||
		}),
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default (params: any) => new Promise(async (res, rej) => {
 | 
			
		||||
  const [ps, psErr] = getParams(meta, params);
 | 
			
		||||
  if (psErr) return rej(psErr);
 | 
			
		||||
	const [ps, psErr] = getParams(meta, params);
 | 
			
		||||
	if (psErr) return rej(psErr);
 | 
			
		||||
 | 
			
		||||
  const user = await User.findOne({
 | 
			
		||||
    _id: ps.userId
 | 
			
		||||
  });
 | 
			
		||||
	const user = await User.findOne({
 | 
			
		||||
		_id: ps.userId
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
  if (user == null) {
 | 
			
		||||
    return rej('user not found');
 | 
			
		||||
  }
 | 
			
		||||
	if (user == null) {
 | 
			
		||||
		return rej('user not found');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  await User.findOneAndUpdate({
 | 
			
		||||
    _id: user._id
 | 
			
		||||
  }, {
 | 
			
		||||
      $set: {
 | 
			
		||||
        isSuspended: true
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
	await User.findOneAndUpdate({
 | 
			
		||||
		_id: user._id
 | 
			
		||||
	}, {
 | 
			
		||||
			$set: {
 | 
			
		||||
				isSuspended: true
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
  res();
 | 
			
		||||
	res();
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,7 @@ export default () => new Promise(async (res, rej) => {
 | 
			
		|||
			model: os.cpus()[0].model,
 | 
			
		||||
			cores: os.cpus().length
 | 
			
		||||
		},
 | 
			
		||||
		broadcasts: meta.broadcasts
 | 
			
		||||
		broadcasts: meta.broadcasts,
 | 
			
		||||
		disableRegistration: meta.disableRegistration
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@ import User, { IUser, validateUsername, validatePassword, pack } from '../../../
 | 
			
		|||
import generateUserToken from '../common/generate-native-user-token';
 | 
			
		||||
import config from '../../../config';
 | 
			
		||||
import Meta from '../../../models/meta';
 | 
			
		||||
import RegistrationTicket from '../../../models/registration-tickets';
 | 
			
		||||
 | 
			
		||||
if (config.recaptcha) {
 | 
			
		||||
	recaptcha.init({
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +30,29 @@ export default async (ctx: Koa.Context) => {
 | 
			
		|||
 | 
			
		||||
	const username = body['username'];
 | 
			
		||||
	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
 | 
			
		||||
	if (!validateUsername(username)) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue