Added signup page.
This commit is contained in:
		
							parent
							
								
									f32722ee7c
								
							
						
					
					
						commit
						082d4fc154
					
				|  | @ -138,7 +138,7 @@ function search() { | ||||||
| 
 | 
 | ||||||
| async function logout() { | async function logout() { | ||||||
| 	del('/session'); | 	del('/session'); | ||||||
| 	window.location.reload(); | 	navigate('/login', null, { redirect: true }); | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -98,13 +98,13 @@ async function login() { | ||||||
| 	errorMsg.value = null; | 	errorMsg.value = null; | ||||||
| 
 | 
 | ||||||
| 	try { | 	try { | ||||||
| 		await post('/session', { | 		const loginUser = await post('/session', { | ||||||
| 			username: username.value, | 			username: username.value, | ||||||
| 			password: password.value, | 			password: password.value, | ||||||
| 			redirect: pageContext.urlParsed.search.r, | 			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) { | 	} catch (error) { | ||||||
| 		errorMsg.value = error.message; | 		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 pageContext = inject('pageContext'); | ||||||
| const profile = pageContext.pageProps.profile; | const profile = pageContext.pageProps.profile; | ||||||
| 
 |  | ||||||
| console.log('profile', profile); |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
|  |  | ||||||
|  | @ -1,8 +1,14 @@ | ||||||
|  | import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */ | ||||||
|  | 
 | ||||||
| import { fetchUser } from '#/src/users.js'; | import { fetchUser } from '#/src/users.js'; | ||||||
| 
 | 
 | ||||||
| export async function onBeforeRender(pageContext) { | export async function onBeforeRender(pageContext) { | ||||||
| 	const profile = await fetchUser(pageContext.routeParams.username); | 	const profile = await fetchUser(pageContext.routeParams.username); | ||||||
| 
 | 
 | ||||||
|  | 	if (!profile) { | ||||||
|  | 		throw render(404, `Cannot find user '${pageContext.routeParams.username}'.`); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return { | 	return { | ||||||
| 		pageContext: { | 		pageContext: { | ||||||
| 			title: profile.username, | 			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); | 		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) { | 	if (!user) { | ||||||
| 		throw new HttpError('Username or password incorrect', 401); | 		throw new HttpError('Username or password incorrect', 401); | ||||||
|  | @ -92,8 +95,8 @@ export async function signup(credentials) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const existingUser = await knex('users') | 	const existingUser = await knex('users') | ||||||
| 		.where('username', curatedUsername) | 		.where(knex.raw('lower(username)'), curatedUsername.toLowerCase()) | ||||||
| 		.orWhere('email', credentials.email) | 		.orWhere(knex.raw('lower(email)'), credentials.email.toLowerCase()) | ||||||
| 		.first(); | 		.first(); | ||||||
| 
 | 
 | ||||||
| 	if (existingUser) { | 	if (existingUser) { | ||||||
|  | @ -120,5 +123,10 @@ export async function signup(credentials) { | ||||||
| 		primary: true, | 		primary: true, | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | 	await generateAvatar({ | ||||||
|  | 		id: userId, | ||||||
|  | 		username: curatedUsername, | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
| 	return fetchUser(userId); | 	return fetchUser(userId); | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								src/users.js
								
								
								
								
							
							
						
						
									
										12
									
								
								src/users.js
								
								
								
								
							|  | @ -23,7 +23,7 @@ export function curateUser(user) { | ||||||
| 	return curatedUser; | 	return curatedUser; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function fetchUser(userId, raw) { | export async function fetchUser(userId, options = {}) { | ||||||
| 	const user = await knex('users') | 	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')) | 		.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) => { | 		.modify((builder) => { | ||||||
|  | @ -32,9 +32,11 @@ export async function fetchUser(userId, raw) { | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if (typeof userId === 'string') { | 			if (typeof userId === 'string') { | ||||||
| 				builder | 				builder.where(knex.raw('lower(users.username)'), userId.toLowerCase()); | ||||||
| 					.where('users.username', userId) | 
 | ||||||
| 					.orWhere('users.email', userId); | 				if (options.email) { | ||||||
|  | 					builder.orWhere(knex.raw('lower(users.email)'), userId.toLowerCase()); | ||||||
|  | 				} | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 		.leftJoin('users_roles', 'users_roles.role', 'users.role') | 		.leftJoin('users_roles', 'users_roles.role', 'users.role') | ||||||
|  | @ -42,7 +44,7 @@ export async function fetchUser(userId, raw) { | ||||||
| 		.groupBy('users.id', 'users_roles.role') | 		.groupBy('users.id', 'users_roles.role') | ||||||
| 		.first(); | 		.first(); | ||||||
| 
 | 
 | ||||||
| 	if (raw) { | 	if (options.raw) { | ||||||
| 		return user; | 		return user; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,8 +11,6 @@ export async function setUserApi(req, res, next) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function loginApi(req, res) { | export async function loginApi(req, res) { | ||||||
| 	console.log('login!', req.body); |  | ||||||
| 
 |  | ||||||
| 	const user = await login(req.body); | 	const user = await login(req.body); | ||||||
| 
 | 
 | ||||||
| 	req.session.user = user; | 	req.session.user = user; | ||||||
|  |  | ||||||
|  | @ -34,6 +34,7 @@ import { | ||||||
| 	setUserApi, | 	setUserApi, | ||||||
| 	loginApi, | 	loginApi, | ||||||
| 	logoutApi, | 	logoutApi, | ||||||
|  | 	signupApi, | ||||||
| } from './auth.js'; | } from './auth.js'; | ||||||
| 
 | 
 | ||||||
| import initLogger from '../logger.js'; | import initLogger from '../logger.js'; | ||||||
|  | @ -89,21 +90,22 @@ export default async function initServer() { | ||||||
| 		router.use(viteDevMiddleware); | 		router.use(viteDevMiddleware); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	router.get('/api/scenes', fetchScenesApi); | 	// SESSION
 | ||||||
| 
 |  | ||||||
| 	router.get('/api/actors', fetchActorsApi); |  | ||||||
| 
 |  | ||||||
| 	router.get('/api/movies', fetchMoviesApi); |  | ||||||
| 
 |  | ||||||
| 	router.post('/api/session', loginApi); | 	router.post('/api/session', loginApi); | ||||||
| 	router.delete('/api/session', logoutApi); | 	router.delete('/api/session', logoutApi); | ||||||
| 
 | 
 | ||||||
| 	// ...
 | 	// USERS
 | ||||||
| 	// Other middlewares (e.g. some RPC middleware such as Telefunc)
 | 	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) => { | 	router.get('*', async (req, res, next) => { | ||||||
| 		const pageContextInit = { | 		const pageContextInit = { | ||||||
| 			urlOriginal: req.originalUrl, | 			urlOriginal: req.originalUrl, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue