mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-31 05:24:13 +00:00 
			
		
		
		
	
							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
		
		Reference in a new issue