parent
							
								
									dcdb57df9d
								
							
						
					
					
						commit
						2c8f962889
					
				
					 9 changed files with 124 additions and 33 deletions
				
			
		| 
						 | 
					@ -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: "利用できます"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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', {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,4 +11,5 @@ export type IMeta = {
 | 
				
			||||||
		usersCount: number;
 | 
							usersCount: number;
 | 
				
			||||||
		originalUsersCount: 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
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue