Added signup page.
This commit is contained in:
		
							parent
							
								
									f32722ee7c
								
							
						
					
					
						commit
						082d4fc154
					
				|  | @ -138,7 +138,7 @@ function search() { | |||
| 
 | ||||
| async function logout() { | ||||
| 	del('/session'); | ||||
| 	window.location.reload(); | ||||
| 	navigate('/login', null, { redirect: true }); | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -98,13 +98,13 @@ async function login() { | |||
| 	errorMsg.value = null; | ||||
| 
 | ||||
| 	try { | ||||
| 		await post('/session', { | ||||
| 		const loginUser = await post('/session', { | ||||
| 			username: username.value, | ||||
| 			password: password.value, | ||||
| 			redirect: pageContext.urlParsed.search.r, | ||||
| 		}); | ||||
| 
 | ||||
| 		navigate(decodeURIComponent(pageContext.urlParsed.search.r), null, { redirect: true }); | ||||
| 		navigate(pageContext.urlParsed.search.r ? decodeURIComponent(pageContext.urlParsed.search.r) : `/user/${loginUser.username}`, null, { redirect: true }); | ||||
| 	} catch (error) { | ||||
| 		errorMsg.value = error.message; | ||||
| 	} | ||||
|  |  | |||
|  | @ -0,0 +1,236 @@ | |||
| <template> | ||||
| 	<div class="login-container"> | ||||
| 		<div v-if="user"> | ||||
| 			You are already logged in as {{ user.username }}. | ||||
| 
 | ||||
| 			<ul> | ||||
| 				<li> | ||||
| 					<a | ||||
| 						:href="`/user/${user.username}`" | ||||
| 						class="link" | ||||
| 					>View my profile</a> | ||||
| 				</li> | ||||
| 
 | ||||
| 				<li> | ||||
| 					<a | ||||
| 						:href="`/updates`" | ||||
| 						class="link" | ||||
| 					>Check out latest porn updates</a> | ||||
| 				</li> | ||||
| 
 | ||||
| 				<li> | ||||
| 					<a | ||||
| 						:href="`/actors`" | ||||
| 						class="link" | ||||
| 					>Browse the hottest porn stars</a> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<form | ||||
| 			v-else | ||||
| 			autocomplete="off" | ||||
| 			class="login-panel" | ||||
| 			@submit.prevent="signup" | ||||
| 		> | ||||
| 			<div | ||||
| 				v-if="errorMsg" | ||||
| 				class="error" | ||||
| 			>{{ errorMsg }}</div> | ||||
| 
 | ||||
| 			<input | ||||
| 				v-model="username" | ||||
| 				placeholder="Username" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<input | ||||
| 				v-model="email" | ||||
| 				type="email" | ||||
| 				placeholder="E-mail" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<div class="password-container"> | ||||
| 				<input | ||||
| 					v-model="password" | ||||
| 					:type="showPassword ? 'input' : 'password'" | ||||
| 					autocomplete="new-password" | ||||
| 					minlength="3" | ||||
| 					placeholder="Password" | ||||
| 					class="password input" | ||||
| 					required | ||||
| 				> | ||||
| 
 | ||||
| 				<div | ||||
| 					class="password-show" | ||||
| 					@click="showPassword = !showPassword" | ||||
| 				> | ||||
| 					<Icon | ||||
| 						v-show="!showPassword" | ||||
| 						icon="eye" | ||||
| 						class="password-show" | ||||
| 					/> | ||||
| 
 | ||||
| 					<Icon | ||||
| 						v-show="showPassword" | ||||
| 						icon="eye-blocked" | ||||
| 						class="password-show" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="password-container"> | ||||
| 				<input | ||||
| 					v-model="passwordConfirm" | ||||
| 					:type="showPassword ? 'input' : 'password'" | ||||
| 					autocomplete="new-password" | ||||
| 					minlength="3" | ||||
| 					placeholder="Confirm password" | ||||
| 					class="password input" | ||||
| 					required | ||||
| 				> | ||||
| 
 | ||||
| 				<div | ||||
| 					class="password-show" | ||||
| 					@click="showPassword = !showPassword" | ||||
| 				> | ||||
| 					<Icon | ||||
| 						v-show="!showPassword" | ||||
| 						icon="eye" | ||||
| 						class="password-show" | ||||
| 					/> | ||||
| 
 | ||||
| 					<Icon | ||||
| 						v-show="showPassword" | ||||
| 						icon="eye-blocked" | ||||
| 						class="password-show" | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<button class="button button-submit">Sign up</button> | ||||
| 
 | ||||
| 			<a | ||||
| 				href="/login" | ||||
| 				class="link" | ||||
| 			>I already have an account</a> | ||||
| 		</form> | ||||
| 	</div> | ||||
| </template> | ||||
| 
 | ||||
| <script setup> | ||||
| import { ref, inject } from 'vue'; | ||||
| 
 | ||||
| import { post } from '#/src/api.js'; | ||||
| import navigate from '#/src/navigate.js'; | ||||
| 
 | ||||
| const pageContext = inject('pageContext'); | ||||
| const user = pageContext.user; | ||||
| 
 | ||||
| const username = ref(''); | ||||
| const email = ref(''); | ||||
| const password = ref(''); | ||||
| const passwordConfirm = ref(''); | ||||
| 
 | ||||
| const errorMsg = ref(null); | ||||
| const showPassword = ref(false); | ||||
| 
 | ||||
| async function signup() { | ||||
| 	errorMsg.value = null; | ||||
| 
 | ||||
| 	if (password.value !== passwordConfirm.value) { | ||||
| 		errorMsg.value = 'Passwords do not match'; | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	try { | ||||
| 		const newUser = await post('/users', { | ||||
| 			username: username.value, | ||||
| 			email: email.value, | ||||
| 			password: password.value, | ||||
| 			redirect: pageContext.urlParsed.search.r, | ||||
| 		}); | ||||
| 
 | ||||
| 		navigate(`/user/${newUser.username}`, null, { redirect: true }); | ||||
| 	} catch (error) { | ||||
| 		errorMsg.value = error.message; | ||||
| 	} | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .login-container { | ||||
| 	display: flex; | ||||
| 	flex-grow: 1; | ||||
| 	justify-content: center; | ||||
| 	align-items: center; | ||||
| 	background: var(--background-base-10); | ||||
| } | ||||
| 
 | ||||
| .login-panel { | ||||
| 	width: 20rem; | ||||
| 	max-width: 100%; | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	gap: .5rem; | ||||
| 	padding: 1rem; | ||||
| 	margin: 1rem; | ||||
| 	border-radius: .5rem; | ||||
| 	box-shadow: 0 0 3px var(--shadow-weak-30); | ||||
| 	background: var(--background-base); | ||||
| 	font-size: 1rem; | ||||
| 
 | ||||
| 	.button { | ||||
| 		justify-content: center; | ||||
| 	} | ||||
| 
 | ||||
| 	.link { | ||||
| 		margin-top: .5rem; | ||||
| 		text-align: center; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .password-container { | ||||
| 	display: flex; | ||||
| 	position: relative; | ||||
| } | ||||
| 
 | ||||
| .password { | ||||
| 	flex-grow: 1; | ||||
| } | ||||
| 
 | ||||
| .password-show { | ||||
| 	height: 100%; | ||||
| 	display: flex; | ||||
| 	align-items: center; | ||||
| 	position: absolute; | ||||
| 	right: 0; | ||||
| 	padding: 0 1rem 0 .5rem; | ||||
| 
 | ||||
| 	.icon { | ||||
| 		fill: var(--shadow); | ||||
| 	} | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		cursor: pointer; | ||||
| 
 | ||||
| 		.icon { | ||||
| 			fill: var(--primary); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .error { | ||||
| 	background: var(--error); | ||||
| 	color: var(--text-light); | ||||
| 	padding: .75rem 1rem; | ||||
| 	border-radius: .25rem; | ||||
| 	margin-bottom: .5rem; | ||||
| 	font-size: .9rem; | ||||
| 	font-weight: bold; | ||||
| 	text-align: center; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1 @@ | |||
| export default '/signup'; | ||||
|  | @ -17,8 +17,6 @@ import { inject } from 'vue'; | |||
| 
 | ||||
| const pageContext = inject('pageContext'); | ||||
| const profile = pageContext.pageProps.profile; | ||||
| 
 | ||||
| console.log('profile', profile); | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
|  |  | |||
|  | @ -1,8 +1,14 @@ | |||
| import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */ | ||||
| 
 | ||||
| import { fetchUser } from '#/src/users.js'; | ||||
| 
 | ||||
| export async function onBeforeRender(pageContext) { | ||||
| 	const profile = await fetchUser(pageContext.routeParams.username); | ||||
| 
 | ||||
| 	if (!profile) { | ||||
| 		throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`); | ||||
| 	} | ||||
| 
 | ||||
| 	return { | ||||
| 		pageContext: { | ||||
| 			title: profile.username, | ||||
|  |  | |||
							
								
								
									
										14
									
								
								src/auth.js
								
								
								
								
							
							
						
						
									
										14
									
								
								src/auth.js
								
								
								
								
							|  | @ -41,7 +41,10 @@ export async function login(credentials) { | |||
| 		throw new HttpError('Authentication is disabled', 405); | ||||
| 	} | ||||
| 
 | ||||
| 	const user = await fetchUser(credentials.username.trim(), true); | ||||
| 	const user = await fetchUser(credentials.username.trim(), { | ||||
| 		email: true, | ||||
| 		raw: true, | ||||
| 	}); | ||||
| 
 | ||||
| 	if (!user) { | ||||
| 		throw new HttpError('Username or password incorrect', 401); | ||||
|  | @ -92,8 +95,8 @@ export async function signup(credentials) { | |||
| 	} | ||||
| 
 | ||||
| 	const existingUser = await knex('users') | ||||
| 		.where('username', curatedUsername) | ||||
| 		.orWhere('email', credentials.email) | ||||
| 		.where(knex.raw('lower(username)'), curatedUsername.toLowerCase()) | ||||
| 		.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase()) | ||||
| 		.first(); | ||||
| 
 | ||||
| 	if (existingUser) { | ||||
|  | @ -120,5 +123,10 @@ export async function signup(credentials) { | |||
| 		primary: true, | ||||
| 	}); | ||||
| 
 | ||||
| 	await generateAvatar({ | ||||
| 		id: userId, | ||||
| 		username: curatedUsername, | ||||
| 	}); | ||||
| 
 | ||||
| 	return fetchUser(userId); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										12
									
								
								src/users.js
								
								
								
								
							
							
						
						
									
										12
									
								
								src/users.js
								
								
								
								
							|  | @ -23,7 +23,7 @@ export function curateUser(user) { | |||
| 	return curatedUser; | ||||
| } | ||||
| 
 | ||||
| export async function fetchUser(userId, raw) { | ||||
| export async function fetchUser(userId, options = {}) { | ||||
| 	const user = await knex('users') | ||||
| 		.select(knex.raw('users.*, users_roles.abilities as role_abilities, COALESCE(json_agg(stashes ORDER BY stashes.created_at) FILTER (WHERE stashes.id IS NOT NULL), \'[]\') as stashes')) | ||||
| 		.modify((builder) => { | ||||
|  | @ -32,9 +32,11 @@ export async function fetchUser(userId, raw) { | |||
| 			} | ||||
| 
 | ||||
| 			if (typeof userId === 'string') { | ||||
| 				builder | ||||
| 					.where('users.username', userId) | ||||
| 					.orWhere('users.email', userId); | ||||
| 				builder.where(knex.raw('lower(users.username)'), userId.toLowerCase()); | ||||
| 
 | ||||
| 				if (options.email) { | ||||
| 					builder.orWhere(knex.raw('lower(users.email)'), userId.toLowerCase()); | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 		.leftJoin('users_roles', 'users_roles.role', 'users.role') | ||||
|  | @ -42,7 +44,7 @@ export async function fetchUser(userId, raw) { | |||
| 		.groupBy('users.id', 'users_roles.role') | ||||
| 		.first(); | ||||
| 
 | ||||
| 	if (raw) { | ||||
| 	if (options.raw) { | ||||
| 		return user; | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,8 +11,6 @@ export async function setUserApi(req, res, next) { | |||
| } | ||||
| 
 | ||||
| export async function loginApi(req, res) { | ||||
| 	console.log('login!', req.body); | ||||
| 
 | ||||
| 	const user = await login(req.body); | ||||
| 
 | ||||
| 	req.session.user = user; | ||||
|  |  | |||
|  | @ -34,6 +34,7 @@ import { | |||
| 	setUserApi, | ||||
| 	loginApi, | ||||
| 	logoutApi, | ||||
| 	signupApi, | ||||
| } from './auth.js'; | ||||
| 
 | ||||
| import initLogger from '../logger.js'; | ||||
|  | @ -89,21 +90,22 @@ export default async function initServer() { | |||
| 		router.use(viteDevMiddleware); | ||||
| 	} | ||||
| 
 | ||||
| 	router.get('/api/scenes', fetchScenesApi); | ||||
| 
 | ||||
| 	router.get('/api/actors', fetchActorsApi); | ||||
| 
 | ||||
| 	router.get('/api/movies', fetchMoviesApi); | ||||
| 
 | ||||
| 	// SESSION
 | ||||
| 	router.post('/api/session', loginApi); | ||||
| 	router.delete('/api/session', logoutApi); | ||||
| 
 | ||||
| 	// ...
 | ||||
| 	// Other middlewares (e.g. some RPC middleware such as Telefunc)
 | ||||
| 	// ...
 | ||||
| 	// USERS
 | ||||
| 	router.post('/api/users', signupApi); | ||||
| 
 | ||||
| 	// SCENES
 | ||||
| 	router.get('/api/scenes', fetchScenesApi); | ||||
| 
 | ||||
| 	// ACTORS
 | ||||
| 	router.get('/api/actors', fetchActorsApi); | ||||
| 
 | ||||
| 	// MOVIES
 | ||||
| 	router.get('/api/movies', fetchMoviesApi); | ||||
| 
 | ||||
| 	// Vike middleware. It should always be our last middleware (because it's a
 | ||||
| 	// catch-all middleware superseding any middleware placed after it).
 | ||||
| 	router.get('*', async (req, res, next) => { | ||||
| 		const pageContextInit = { | ||||
| 			urlOriginal: req.originalUrl, | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue