Added user sign up and login.
|  | @ -0,0 +1,116 @@ | |||
| <template> | ||||
| 	<form | ||||
| 		class="login" | ||||
| 		@submit.prevent="login" | ||||
| 	> | ||||
| 		<div | ||||
| 			v-if="error" | ||||
| 			class="feedback error" | ||||
| 		>{{ error }}</div> | ||||
| 
 | ||||
| 		<div | ||||
| 			v-if="success" | ||||
| 			class="feedback success" | ||||
| 		>Login successful, redirecting</div> | ||||
| 
 | ||||
| 		<template v-else> | ||||
| 			<input | ||||
| 				v-model="username" | ||||
| 				placeholder="Username or e-mail" | ||||
| 				type="text" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<input | ||||
| 				v-model="password" | ||||
| 				placeholder="Password" | ||||
| 				type="password" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<button | ||||
| 				type="submit" | ||||
| 				class="button button-primary" | ||||
| 			>Log in</button> | ||||
| 
 | ||||
| 			<router-link | ||||
| 				to="/signup" | ||||
| 				class="link link-external signup" | ||||
| 			>Sign up</router-link> | ||||
| 		</template> | ||||
| 	</form> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| async function login() { | ||||
| 	this.error = null; | ||||
| 	this.success = false; | ||||
| 
 | ||||
| 	try { | ||||
| 		await this.$store.dispatch('login', { | ||||
| 			username: this.username, | ||||
| 			password: this.password, | ||||
| 		}); | ||||
| 
 | ||||
| 		this.success = true; | ||||
| 
 | ||||
| 		setTimeout(() => { | ||||
| 			this.$router.replace(this.$route.query.ref || { name: 'home' }); | ||||
| 		}, 1000); | ||||
| 	} catch (error) { | ||||
| 		this.error = error.message; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
| 	data() { | ||||
| 		return { | ||||
| 			username: null, | ||||
| 			password: null, | ||||
| 			success: false, | ||||
| 			error: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		login, | ||||
| 	}, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .login { | ||||
| 	width: 20rem; | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	margin: auto; | ||||
| } | ||||
| 
 | ||||
| .input { | ||||
| 	margin: 0 0 .5rem 0; | ||||
| } | ||||
| 
 | ||||
| .feedback { | ||||
| 	padding: 1rem; | ||||
| 	text-align: center; | ||||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .error { | ||||
| 	color: var(--error); | ||||
| } | ||||
| 
 | ||||
| .success { | ||||
| 	color: var(--shadow-strong); | ||||
| } | ||||
| 
 | ||||
| .button { | ||||
| 	margin: 0 0 .25rem 0; | ||||
| } | ||||
| 
 | ||||
| .signup { | ||||
| 	padding: .5rem; | ||||
| 	text-align: center; | ||||
| } | ||||
| </style> | ||||
|  | @ -0,0 +1,126 @@ | |||
| <template> | ||||
| 	<form | ||||
| 		class="signup" | ||||
| 		@submit.prevent="signup" | ||||
| 	> | ||||
| 		<div | ||||
| 			v-if="error" | ||||
| 			class="feedback error" | ||||
| 		>{{ error }}</div> | ||||
| 
 | ||||
| 		<div | ||||
| 			v-if="success" | ||||
| 			class="feedback success" | ||||
| 		>Signup successful, redirecting</div> | ||||
| 
 | ||||
| 		<template v-else> | ||||
| 			<input | ||||
| 				v-model="username" | ||||
| 				placeholder="Username" | ||||
| 				type="text" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<input | ||||
| 				v-model="email" | ||||
| 				placeholder="E-mail" | ||||
| 				type="email" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<input | ||||
| 				v-model="password" | ||||
| 				placeholder="Password" | ||||
| 				type="password" | ||||
| 				class="input" | ||||
| 				required | ||||
| 			> | ||||
| 
 | ||||
| 			<button | ||||
| 				type="submit" | ||||
| 				class="button button-primary" | ||||
| 			>Sign up</button> | ||||
| 
 | ||||
| 			<router-link | ||||
| 				to="/login" | ||||
| 				class="link link-external login" | ||||
| 			>Log in</router-link> | ||||
| 		</template> | ||||
| 	</form> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| async function signup() { | ||||
| 	this.error = null; | ||||
| 	this.success = false; | ||||
| 
 | ||||
| 	try { | ||||
| 		await this.$store.dispatch('signup', { | ||||
| 			username: this.username, | ||||
| 			email: this.email, | ||||
| 			password: this.password, | ||||
| 		}); | ||||
| 
 | ||||
| 		this.success = true; | ||||
| 
 | ||||
| 		setTimeout(() => { | ||||
| 			this.$router.replace(this.$route.query.ref || { name: 'home' }); | ||||
| 		}, 1000); | ||||
| 	} catch (error) { | ||||
| 		this.error = error.message; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
| 	data() { | ||||
| 		return { | ||||
| 			username: null, | ||||
| 			email: null, | ||||
| 			password: null, | ||||
| 			success: false, | ||||
| 			error: null, | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
| 		signup, | ||||
| 	}, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .signup { | ||||
| 	width: 20rem; | ||||
| 	display: flex; | ||||
| 	flex-direction: column; | ||||
| 	margin: auto; | ||||
| } | ||||
| 
 | ||||
| .input { | ||||
| 	margin: 0 0 .5rem 0; | ||||
| } | ||||
| 
 | ||||
| .feedback { | ||||
| 	padding: 1rem; | ||||
| 	text-align: center; | ||||
| 	font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .error { | ||||
| 	color: var(--error); | ||||
| } | ||||
| 
 | ||||
| .success { | ||||
| 	color: var(--shadow-strong); | ||||
| } | ||||
| 
 | ||||
| .button { | ||||
| 	margin: 0 0 .25rem 0; | ||||
| } | ||||
| 
 | ||||
| .login { | ||||
| 	padding: .5rem; | ||||
| 	text-align: center; | ||||
| } | ||||
| </style> | ||||
|  | @ -95,11 +95,26 @@ | |||
| 				<template v-slot:tooltip> | ||||
| 					<div class="menu"> | ||||
| 						<ul class="menu-items noselect"> | ||||
| 							<li | ||||
| 								class="menu-item disabled" | ||||
| 							<router-link | ||||
| 								v-if="!me" | ||||
| 								to="/login" | ||||
| 								class="menu-item" | ||||
| 								@click.stop | ||||
| 							> | ||||
| 								<Icon icon="enter2" />Sign in | ||||
| 								<Icon icon="enter2" />Log in | ||||
| 							</router-link> | ||||
| 
 | ||||
| 							<li | ||||
| 								v-if="me" | ||||
| 								class="menu-username" | ||||
| 							>{{ me.username }}</li> | ||||
| 
 | ||||
| 							<li | ||||
| 								v-if="me" | ||||
| 								class="menu-item" | ||||
| 								@click.stop="$store.dispatch('logout')" | ||||
| 							> | ||||
| 								<Icon icon="enter2" />Log out | ||||
| 							</li> | ||||
| 
 | ||||
| 							<li | ||||
|  | @ -200,6 +215,10 @@ function theme(state) { | |||
| 	return state.ui.theme; | ||||
| } | ||||
| 
 | ||||
| function me(state) { | ||||
| 	return state.auth.user; | ||||
| } | ||||
| 
 | ||||
| function setTheme(newTheme) { | ||||
| 	this.$store.dispatch('setTheme', newTheme); | ||||
| } | ||||
|  | @ -224,6 +243,7 @@ export default { | |||
| 		...mapState({ | ||||
| 			sfw, | ||||
| 			theme, | ||||
| 			me, | ||||
| 		}), | ||||
| 	}, | ||||
| 	methods: { | ||||
|  | @ -403,6 +423,8 @@ export default { | |||
| .menu-item { | ||||
| 	display: flex; | ||||
| 	padding: .75rem 1rem .75rem .75rem; | ||||
| 	color: inherit; | ||||
| 	text-decoration: none; | ||||
| 
 | ||||
| 	.icon { | ||||
| 		fill: var(--darken); | ||||
|  | @ -428,6 +450,15 @@ export default { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| .menu-username { | ||||
| 	font-weight: bold; | ||||
| 	color: var(--shadow-strong); | ||||
| 	font-size: .9rem; | ||||
| 	padding: .75rem 1rem; | ||||
| 	border-bottom: solid 1px var(--shadow-hint); | ||||
| 	text-align: center; | ||||
| } | ||||
| 
 | ||||
| .search-compact { | ||||
|     display: none; | ||||
|     height: 100%; | ||||
|  |  | |||
|  | @ -1,11 +1,15 @@ | |||
| .input { | ||||
| 	box-sizing: border-box; | ||||
| 	padding: .5rem; | ||||
| 	border: solid 1px var(--shadow-weak); | ||||
| 	border: solid 1px var(--shadow-hint); | ||||
| 	color: var(--shadow-strong); | ||||
| 	background: var(--background); | ||||
| 	font-size: 1rem; | ||||
| 
 | ||||
| 	&:focus { | ||||
| 		border: solid 1px var(--primary); | ||||
| 	} | ||||
| 
 | ||||
| 	&::-webkit-calendar-picker-indicator { | ||||
| 		opacity: .5; | ||||
| 	} | ||||
|  | @ -20,6 +24,35 @@ | |||
|     cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .button { | ||||
| 	border: none; | ||||
| 	background: none; | ||||
| 	padding: .5rem; | ||||
| 	font-weight: bold; | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		cursor: pointer; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .button-primary { | ||||
| 	color: var(--text-light); | ||||
| 	background: var(--primary); | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		background: var(--primary-strong); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .button-secondary { | ||||
| 	color: var(--primary); | ||||
| 
 | ||||
| 	&:hover { | ||||
| 		color: var(--highlight-strong); | ||||
| 		background: var(--primary); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| .album-toggle { | ||||
| 	height: fit-content; | ||||
| 	display: inline-flex; | ||||
|  |  | |||
|  | @ -36,6 +36,7 @@ $breakpoint4: 1500px; | |||
|     --female: #f0a; | ||||
| 
 | ||||
|     --alert: #f00; | ||||
|     --error: #f00; | ||||
|     --warn: #fa0; | ||||
|     --success: #5c2; | ||||
| 
 | ||||
|  | @ -58,6 +59,7 @@ $breakpoint4: 1500px; | |||
|     --tile: #2a2a2a; | ||||
| 
 | ||||
|     --link: #dd6688; | ||||
|     --link-external: #48f; | ||||
|     --empty: #333; | ||||
| 
 | ||||
| 	--crease: #eaeaea; | ||||
|  |  | |||
|  | @ -44,3 +44,12 @@ body { | |||
|         fill: var(--primary); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .link { | ||||
| 	color: var(--link); | ||||
| 	text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| .link-external { | ||||
| 	color: var(--link-external); | ||||
| } | ||||
|  |  | |||
|  | @ -39,6 +39,22 @@ async function post(endpoint, data) { | |||
| 	throw new Error(errorMsg); | ||||
| } | ||||
| 
 | ||||
| async function del(endpoint) { | ||||
| 	const res = await fetch(`${config.api.url}${endpoint}`, { | ||||
| 		method: 'DELETE', | ||||
| 		mode: 'cors', | ||||
| 		credentials: 'same-origin', | ||||
| 	}); | ||||
| 
 | ||||
| 	if (res.ok) { | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	const errorMsg = await res.text(); | ||||
| 
 | ||||
| 	throw new Error(errorMsg); | ||||
| } | ||||
| 
 | ||||
| async function graphql(query, variables = null) { | ||||
| 	const res = await fetch('/graphql', { | ||||
| 		method: 'POST', | ||||
|  | @ -67,5 +83,6 @@ async function graphql(query, variables = null) { | |||
| export { | ||||
| 	get, | ||||
| 	post, | ||||
| 	del, | ||||
| 	graphql, | ||||
| }; | ||||
|  |  | |||
|  | @ -1,3 +1,42 @@ | |||
| function initAuthActions(_store, _router) {} | ||||
| import { get, post, del } from '../api'; | ||||
| 
 | ||||
| function initAuthActions(_store, _router) { | ||||
| 	async function fetchMe({ commit }) { | ||||
| 		const user = await get('/session'); | ||||
| 
 | ||||
| 		commit('setUser', user); | ||||
| 
 | ||||
| 		return user; | ||||
| 	} | ||||
| 
 | ||||
| 	async function login({ commit }, credentials) { | ||||
| 		const user = await post('/session', credentials); | ||||
| 
 | ||||
| 		commit('setUser', user); | ||||
| 
 | ||||
| 		return user; | ||||
| 	} | ||||
| 
 | ||||
| 	async function signup({ commit }, credentials) { | ||||
| 		const user = await post('/users', credentials); | ||||
| 
 | ||||
| 		commit('setUser', user); | ||||
| 
 | ||||
| 		return user; | ||||
| 	} | ||||
| 
 | ||||
| 	async function logout({ commit }) { | ||||
| 		await del('/session'); | ||||
| 
 | ||||
| 		commit('setUser', null); | ||||
| 	} | ||||
| 
 | ||||
| 	return { | ||||
| 		fetchMe, | ||||
| 		login, | ||||
| 		logout, | ||||
| 		signup, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| export default initAuthActions; | ||||
|  |  | |||
|  | @ -1 +1,7 @@ | |||
| export default {}; | ||||
| function setUser(state, user) { | ||||
| 	state.user = user; | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
| 	setUser, | ||||
| }; | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| async function initAuthObserver(store, _router) { | ||||
| 	await store.dispatch('fetchMe'); | ||||
| } | ||||
| 
 | ||||
| export default initAuthObserver; | ||||
|  | @ -1,4 +1,3 @@ | |||
| export default { | ||||
| 	authenticated: false, | ||||
| 	user: null, | ||||
| }; | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import mitt from 'mitt'; | |||
| import router from './router'; | ||||
| import initStore from './store'; | ||||
| import initUiObservers from './ui/observers'; | ||||
| import initAuthObservers from './auth/observers'; | ||||
| 
 | ||||
| import { formatDate, formatDuration } from './format'; | ||||
| 
 | ||||
|  | @ -64,6 +65,7 @@ async function init() { | |||
| 	} | ||||
| 
 | ||||
| 	initUiObservers(store, router); | ||||
| 	initAuthObservers(store, router); | ||||
| 
 | ||||
| 	if (window.env.sfw) { | ||||
| 		store.dispatch('setSfw', true); | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| import { createRouter, createWebHistory } from 'vue-router'; | ||||
| 
 | ||||
| import Home from '../components/home/home.vue'; | ||||
| import Login from '../components/auth/login.vue'; | ||||
| import Signup from '../components/auth/signup.vue'; | ||||
| import Release from '../components/releases/release.vue'; | ||||
| import Entity from '../components/entities/entity.vue'; | ||||
| import Networks from '../components/networks/networks.vue'; | ||||
|  | @ -16,6 +18,7 @@ import NotFound from '../components/errors/404.vue'; | |||
| const routes = [ | ||||
| 	{ | ||||
| 		path: '/', | ||||
| 		name: 'home', | ||||
| 		redirect: { | ||||
| 			name: 'updates', | ||||
| 			params: { | ||||
|  | @ -25,6 +28,16 @@ const routes = [ | |||
| 			}, | ||||
| 		}, | ||||
| 	}, | ||||
| 	{ | ||||
| 		path: '/login', | ||||
| 		name: 'login', | ||||
| 		component: Login, | ||||
| 	}, | ||||
| 	{ | ||||
| 		path: '/signup', | ||||
| 		name: 'singup', | ||||
| 		component: Signup, | ||||
| 	}, | ||||
| 	{ | ||||
| 		path: '/updates', | ||||
| 		redirect: { | ||||
|  |  | |||
|  | @ -105,6 +105,10 @@ exports.up = knex => Promise.resolve() | |||
| 			.inTable('entities') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.date('date'); | ||||
| 		table.enum('date_precision', ['year', 'month', 'day', 'hour', 'minute', 'second']) | ||||
| 			.defaultTo('year'); | ||||
| 
 | ||||
| 		table.text('comment'); | ||||
| 		table.text('group'); | ||||
| 
 | ||||
|  | @ -980,6 +984,69 @@ exports.up = knex => Promise.resolve() | |||
| 
 | ||||
| 		table.unique(['tag_id', 'chapter_id']); | ||||
| 	})) | ||||
| 	.then(() => knex.schema.createTable('users_roles', (table) => { | ||||
| 		table.string('role') | ||||
| 			.primary(); | ||||
| 
 | ||||
| 		table.json('abilities'); | ||||
| 	})) | ||||
| 	.then(() => knex('users_roles').insert([ | ||||
| 		{ | ||||
| 			role: 'admin', | ||||
| 			abilities: JSON.stringify([ // serialization necessary to avoid array being interpreted as a PG array
 | ||||
| 				{ subject: 'scene', action: 'create' }, | ||||
| 				{ subject: 'scene', action: 'update' }, | ||||
| 				{ subject: 'scene', action: 'delete' }, | ||||
| 				{ subject: 'actor', action: 'create' }, | ||||
| 				{ subject: 'actor', action: 'update' }, | ||||
| 				{ subject: 'actor', action: 'delete' }, | ||||
| 			]), | ||||
| 		}, | ||||
| 		{ | ||||
| 			role: 'editor', | ||||
| 			abilities: JSON.stringify([ // serialization necessary to avoid array being interpreted as a PG array
 | ||||
| 				{ subject: 'scene', action: 'update' }, | ||||
| 				{ subject: 'actor', action: 'update' }, | ||||
| 			]), | ||||
| 		}, | ||||
| 		{ | ||||
| 			role: 'user', | ||||
| 		}, | ||||
| 	])) | ||||
| 	.then(() => knex.schema.createTable('users', (table) => { | ||||
| 		table.increments('id'); | ||||
| 
 | ||||
| 		table.text('username') | ||||
| 			.unique() | ||||
| 			.notNullable(); | ||||
| 
 | ||||
| 		table.text('email') | ||||
| 			.unique() | ||||
| 			.notNullable(); | ||||
| 
 | ||||
| 		table.text('password') | ||||
| 			.notNullable(); | ||||
| 
 | ||||
| 		table.string('role') | ||||
| 			.references('role') | ||||
| 			.inTable('users_roles') | ||||
| 			.defaultTo('user') | ||||
| 			.notNullable(); | ||||
| 
 | ||||
| 		table.json('abilities'); | ||||
| 
 | ||||
| 		table.boolean('email_verified') | ||||
| 			.notNullable() | ||||
| 			.defaultTo(false); | ||||
| 
 | ||||
| 		table.boolean('identity_verified') | ||||
| 			.notNullable() | ||||
| 			.defaultTo(false); | ||||
| 
 | ||||
| 		table.datetime('created_at') | ||||
| 			.notNullable() | ||||
| 			.defaultTo(knex.fn.now()); | ||||
| 	})) | ||||
| 	// SEARCH
 | ||||
| 	.then(() => { // eslint-disable-line arrow-body-style
 | ||||
| 		// allow vim fold
 | ||||
|  | @ -1173,6 +1240,9 @@ exports.up = knex => Promise.resolve() | |||
| 	.then(() => { // eslint-disable-line arrow-body-style
 | ||||
| 		// allow vim fold
 | ||||
| 		return knex.raw(` | ||||
| 			COMMENT ON TABLE users IS E'@omit'; | ||||
| 			COMMENT ON TABLE users_roles IS E'@omit'; | ||||
| 
 | ||||
| 			COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many'; | ||||
| 			COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many'; | ||||
| 			COMMENT ON COLUMN actors.penis_length IS E'@omit read,update,create,delete,all,many'; | ||||
|  | @ -1249,6 +1319,9 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style | |||
| 		DROP TABLE IF EXISTS entities_types CASCADE; | ||||
| 		DROP TABLE IF EXISTS entities CASCADE; | ||||
| 
 | ||||
| 		DROP TABLE IF EXISTS users CASCADE; | ||||
| 		DROP TABLE IF EXISTS users_roles CASCADE; | ||||
| 
 | ||||
| 		DROP FUNCTION IF EXISTS search_releases; | ||||
| 		DROP FUNCTION IF EXISTS search_sites; | ||||
| 		DROP FUNCTION IF EXISTS search_entities; | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ | |||
|             "version": "1.184.1", | ||||
|             "license": "ISC", | ||||
|             "dependencies": { | ||||
|                 "@casl/ability": "^5.2.2", | ||||
|                 "@graphile-contrib/pg-order-by-related": "^1.0.0-beta.6", | ||||
|                 "@graphile-contrib/pg-simplify-inflector": "^5.0.0-beta.1", | ||||
|                 "acorn": "^8.0.4", | ||||
|  | @ -1262,6 +1263,17 @@ | |||
|                 "to-fast-properties": "^2.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@casl/ability": { | ||||
|             "version": "5.2.2", | ||||
|             "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-5.2.2.tgz", | ||||
|             "integrity": "sha512-A0GTDWojP72Z4HSgS0pfbtGnhQWbquhn9luAr4Uc/HnqWWib0NvmpXC4//7gsiMUiVYCoFozQ+nG1oeZuhT7Jg==", | ||||
|             "dependencies": { | ||||
|                 "@ucast/mongo2js": "^1.3.0" | ||||
|             }, | ||||
|             "funding": { | ||||
|                 "url": "https://github.com/stalniy/casl/blob/master/BACKERS.md" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@eslint/eslintrc": { | ||||
|             "version": "0.3.0", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", | ||||
|  | @ -1492,6 +1504,37 @@ | |||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@ucast/core": { | ||||
|             "version": "1.8.2", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.8.2.tgz", | ||||
|             "integrity": "sha512-pc+XGjJmZkfypJIIRo38el/FUDtBXBlGQbXafWwRwInocXVwNbJ56efECKLgAQSyI7OCJFSaEeqpf3SrR3D6cw==" | ||||
|         }, | ||||
|         "node_modules/@ucast/js": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.1.tgz", | ||||
|             "integrity": "sha512-sabiuYsM5VUg4EaCwlDxnqcrHPFvbZcXvBu+P/o4pqK2q046RLTdo0bM7iVCn5Ro4HpCiRv3QzxtW8epcluY1g==", | ||||
|             "dependencies": { | ||||
|                 "@ucast/core": "^1.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@ucast/mongo": { | ||||
|             "version": "2.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.1.tgz", | ||||
|             "integrity": "sha512-l/hc3TxjWO9inBrgM5iMCAcsIeV2DToppRlabQa5xB/6uHYtCXfm3TPaJgr8TU1OFxqPlaXEnNQhaV0sVHGsoQ==", | ||||
|             "dependencies": { | ||||
|                 "@ucast/core": "^1.4.1" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@ucast/mongo2js": { | ||||
|             "version": "1.3.2", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.2.tgz", | ||||
|             "integrity": "sha512-KNOEs61wxo4VJkVGqwP2a03TKuLx9fLMQgW5HD8Th/mrcuP1SspS4W+kUQD+wB1AA5pOn65hzlHUw5wZBwme0Q==", | ||||
|             "dependencies": { | ||||
|                 "@ucast/core": "^1.6.1", | ||||
|                 "@ucast/js": "^3.0.0", | ||||
|                 "@ucast/mongo": "^2.4.0" | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/@videojs/http-streaming": { | ||||
|             "version": "2.2.4", | ||||
|             "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.2.4.tgz", | ||||
|  | @ -16603,6 +16646,14 @@ | |||
|                 "to-fast-properties": "^2.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "@casl/ability": { | ||||
|             "version": "5.2.2", | ||||
|             "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-5.2.2.tgz", | ||||
|             "integrity": "sha512-A0GTDWojP72Z4HSgS0pfbtGnhQWbquhn9luAr4Uc/HnqWWib0NvmpXC4//7gsiMUiVYCoFozQ+nG1oeZuhT7Jg==", | ||||
|             "requires": { | ||||
|                 "@ucast/mongo2js": "^1.3.0" | ||||
|             } | ||||
|         }, | ||||
|         "@eslint/eslintrc": { | ||||
|             "version": "0.3.0", | ||||
|             "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", | ||||
|  | @ -16805,6 +16856,37 @@ | |||
|                 "@types/node": "*" | ||||
|             } | ||||
|         }, | ||||
|         "@ucast/core": { | ||||
|             "version": "1.8.2", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.8.2.tgz", | ||||
|             "integrity": "sha512-pc+XGjJmZkfypJIIRo38el/FUDtBXBlGQbXafWwRwInocXVwNbJ56efECKLgAQSyI7OCJFSaEeqpf3SrR3D6cw==" | ||||
|         }, | ||||
|         "@ucast/js": { | ||||
|             "version": "3.0.1", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/js/-/js-3.0.1.tgz", | ||||
|             "integrity": "sha512-sabiuYsM5VUg4EaCwlDxnqcrHPFvbZcXvBu+P/o4pqK2q046RLTdo0bM7iVCn5Ro4HpCiRv3QzxtW8epcluY1g==", | ||||
|             "requires": { | ||||
|                 "@ucast/core": "^1.0.0" | ||||
|             } | ||||
|         }, | ||||
|         "@ucast/mongo": { | ||||
|             "version": "2.4.1", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.1.tgz", | ||||
|             "integrity": "sha512-l/hc3TxjWO9inBrgM5iMCAcsIeV2DToppRlabQa5xB/6uHYtCXfm3TPaJgr8TU1OFxqPlaXEnNQhaV0sVHGsoQ==", | ||||
|             "requires": { | ||||
|                 "@ucast/core": "^1.4.1" | ||||
|             } | ||||
|         }, | ||||
|         "@ucast/mongo2js": { | ||||
|             "version": "1.3.2", | ||||
|             "resolved": "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.2.tgz", | ||||
|             "integrity": "sha512-KNOEs61wxo4VJkVGqwP2a03TKuLx9fLMQgW5HD8Th/mrcuP1SspS4W+kUQD+wB1AA5pOn65hzlHUw5wZBwme0Q==", | ||||
|             "requires": { | ||||
|                 "@ucast/core": "^1.6.1", | ||||
|                 "@ucast/js": "^3.0.0", | ||||
|                 "@ucast/mongo": "^2.4.0" | ||||
|             } | ||||
|         }, | ||||
|         "@videojs/http-streaming": { | ||||
|             "version": "2.2.4", | ||||
|             "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.2.4.tgz", | ||||
|  |  | |||
|  | @ -69,6 +69,7 @@ | |||
|         "webpack-cli": "^3.3.11" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@casl/ability": "^5.2.2", | ||||
|         "@graphile-contrib/pg-order-by-related": "^1.0.0-beta.6", | ||||
|         "@graphile-contrib/pg-simplify-inflector": "^5.0.0-beta.1", | ||||
|         "acorn": "^8.0.4", | ||||
|  |  | |||
| Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB | 
| After Width: | Height: | Size: 7.9 KiB | 
| After Width: | Height: | Size: 5.7 KiB | 
| After Width: | Height: | Size: 6.6 KiB | 
| After Width: | Height: | Size: 6.9 KiB | 
| After Width: | Height: | Size: 6.5 KiB | 
| After Width: | Height: | Size: 6.8 KiB | 
| After Width: | Height: | Size: 6.7 KiB | 
| After Width: | Height: | Size: 1.4 MiB | 
| After Width: | Height: | Size: 2.5 MiB | 
| After Width: | Height: | Size: 3.6 MiB | 
| After Width: | Height: | Size: 32 KiB | 
| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 27 KiB | 
| After Width: | Height: | Size: 2.6 MiB | 
| After Width: | Height: | Size: 2.1 MiB | 
| After Width: | Height: | Size: 2.7 MiB | 
|  | @ -770,12 +770,14 @@ const tagMedia = [ | |||
| 	['enhanced-boobs', 1, 'Lela Star in "Thick"', 'julesjordan'], | ||||
| 	['enhanced-boobs', 18, 'Ebony Godess', 'actiongirls'], | ||||
| 	['enhanced-boobs', 'hunter_bryce_penthouse', 'Hunter Bryce in "On The Bed"', 'Penthouse'], | ||||
| 	['enhanced-boobs', 'trudy_photodromm_1', 'Trudy', 'photodromm'], | ||||
| 	['enhanced-boobs', 'kenzie_anne_playboy', 'Miss Kenzie Anne in "Supercharged"', 'playboy'], | ||||
| 	['enhanced-boobs', 9, 'Putri Cinta', 'watch4beauty'], | ||||
| 	['enhanced-boobs', 3, 'Ashly Anderson', 'passionhd'], | ||||
| 	['enhanced-boobs', 'charlie_atwell_photodromm', 'Charley Atwell', 'photodromm'], | ||||
| 	['enhanced-boobs', '23d', 'Lulu Sex Bomb in "Tropical Touch"'], | ||||
| 	['enhanced-boobs', 22, 'Sakura Sena'], | ||||
| 	['enhanced-boobs', 'mareeva_trudy_photodromm_1', 'Mareeva and Trudy', 'photodromm'], | ||||
| 	['enhanced-boobs', 'shawna_lenee_inthecrack_1', 'Shawna Lenee', 'inthecrack'], | ||||
| 	['enhanced-boobs', 16, 'Marsha May in "Once You Go Black 7"', 'julesjordan'], | ||||
| 	['enhanced-boobs', 'azul_hermosa_pornstarslikeitbig', 'Azul Hermosa in "She Likes Rough Quickies"', 'pornstarslikeitbig'], | ||||
|  | @ -783,7 +785,6 @@ const tagMedia = [ | |||
| 	['enhanced-boobs', 10, 'Tia Cyrus in "Titty-Fucked Yoga Goddess"', 'latinasextapes'], | ||||
| 	['enhanced-boobs', 24, 'Shalina Devine in "Rumbling in the Ring, Part 2"', 'pornworld'], | ||||
| 	['enhanced-boobs', 20, 'Chloe Lamour', 'ddfbusty'], | ||||
| 	['enhanced-boobs', 11, 'Jessa Rhodes and Cali Carter in "Busty Anal Workout"', 'lesbianx'], | ||||
| 	['enhanced-boobs', 13, 'Kitana Lure', 'assholefever'], | ||||
| 	['enhanced-boobs', 8, 'Amber Alena', 'score'], | ||||
| 	['enhanced-boobs', 19, 'Kerrie Lee in "Bricked"', 'studio66tv'], | ||||
|  |  | |||
|  | @ -0,0 +1,80 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const util = require('util'); | ||||
| const crypto = require('crypto'); | ||||
| 
 | ||||
| const knex = require('./knex'); | ||||
| const { curateUser } = require('./users'); | ||||
| const { HttpError } = require('./errors'); | ||||
| 
 | ||||
| const scrypt = util.promisify(crypto.scrypt); | ||||
| 
 | ||||
| async function verifyPassword(password, storedPassword) { | ||||
| 	const [salt, hash] = storedPassword.split('/'); | ||||
| 	const hashedPassword = (await scrypt(password, salt, 64)).toString('hex'); | ||||
| 
 | ||||
| 	if (hashedPassword === hash) { | ||||
| 		return true; | ||||
| 	} | ||||
| 
 | ||||
| 	throw new HttpError('Username or password incorrect', 401); | ||||
| } | ||||
| 
 | ||||
| async function login(credentials) { | ||||
| 	const user = await knex('users') | ||||
| 		.select('users.*', 'users_roles.abilities as role_abilities') | ||||
| 		.where('username', credentials.username) | ||||
| 		.orWhere('email', credentials.username) | ||||
| 		.leftJoin('users_roles', 'users_roles.role', 'users.role') | ||||
| 		.first(); | ||||
| 
 | ||||
| 	if (!user) { | ||||
| 		throw new HttpError('Username or password incorrect', 401); | ||||
| 	} | ||||
| 
 | ||||
| 	await verifyPassword(credentials.password, user.password); | ||||
| 
 | ||||
| 	return curateUser(user); | ||||
| } | ||||
| 
 | ||||
| async function signup(credentials) { | ||||
| 	if (!credentials.username) { | ||||
| 		throw new HttpError('Username required', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (!credentials.email) { | ||||
| 		throw new HttpError('E-mail required', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	if (credentials.password?.length < 3) { | ||||
| 		throw new HttpError('Password must be 3 characters or longer', 400); | ||||
| 	} | ||||
| 
 | ||||
| 	const existingUser = await knex('users') | ||||
| 		.where('username', credentials.username) | ||||
| 		.orWhere('email', credentials.email) | ||||
| 		.first(); | ||||
| 
 | ||||
| 	if (existingUser) { | ||||
| 		throw new HttpError('Username or e-mail already in use', 409); | ||||
| 	} | ||||
| 
 | ||||
| 	const salt = crypto.randomBytes(16).toString('hex'); | ||||
| 	const hashedPassword = (await scrypt(credentials.password, salt, 64)).toString('hex'); | ||||
| 	const storedPassword = `${salt}/${hashedPassword}`; | ||||
| 
 | ||||
| 	const [user] = await knex('users') | ||||
| 		.insert({ | ||||
| 			username: credentials.username, | ||||
| 			email: credentials.email, | ||||
| 			password: storedPassword, | ||||
| 		}) | ||||
| 		.returning('*'); | ||||
| 
 | ||||
| 	return curateUser(user); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	login, | ||||
| 	signup, | ||||
| }; | ||||
|  | @ -0,0 +1,40 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const knex = require('./knex'); | ||||
| 
 | ||||
| function curateUser(user) { | ||||
| 	if (!user) { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	const ability = [...(user.role_abilities || []), ...(user.abilities || [])]; | ||||
| 
 | ||||
| 	const curatedUser = { | ||||
| 		id: user.id, | ||||
| 		username: user.username, | ||||
| 		email: user.email, | ||||
| 		emailVerified: user.email_verified, | ||||
| 		identityVerified: user.identity_verified, | ||||
| 		ability, | ||||
| 		createdAt: user.created_at, | ||||
| 	}; | ||||
| 
 | ||||
| 	return curatedUser; | ||||
| } | ||||
| 
 | ||||
| async function fetchUser(userId) { | ||||
| 	const user = await knex('users') | ||||
| 		.select('users.*', 'users_roles.abilities as role_abilities') | ||||
| 		.where('id', userId) | ||||
| 		.orWhere('username', userId) | ||||
| 		.orWhere('email', userId) | ||||
| 		.leftJoin('users_roles', 'users_roles.role', 'users.role') | ||||
| 		.first(); | ||||
| 
 | ||||
| 	return curateUser(user); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	curateUser, | ||||
| 	fetchUser, | ||||
| }; | ||||
|  | @ -0,0 +1,45 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const { login, signup } = require('../auth'); | ||||
| const { fetchUser } = require('../users'); | ||||
| 
 | ||||
| async function loginApi(req, res) { | ||||
| 	const user = await login(req.body); | ||||
| 
 | ||||
| 	req.session.user = user; | ||||
| 	res.send(user); | ||||
| } | ||||
| 
 | ||||
| async function logoutApi(req, res) { | ||||
| 	req.session.destroy((error) => { | ||||
| 		if (error) { | ||||
| 			res.status(500).send(); | ||||
| 		} | ||||
| 
 | ||||
| 		res.status(204).send(); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| async function fetchMeApi(req, res) { | ||||
| 	if (req.session.user) { | ||||
| 		req.session.user = await fetchUser(req.session.user.id, req.session.user); | ||||
| 
 | ||||
| 		res.send(req.session.user); | ||||
| 		return; | ||||
| 	} | ||||
| 
 | ||||
| 	res.status(401).send(); | ||||
| } | ||||
| 
 | ||||
| async function signupApi(req, res) { | ||||
| 	const user = await signup(req.body); | ||||
| 
 | ||||
| 	res.send(user); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	login: loginApi, | ||||
| 	logout: logoutApi, | ||||
| 	fetchMe: fetchMeApi, | ||||
| 	signup: signupApi, | ||||
| }; | ||||
|  | @ -15,6 +15,13 @@ const errorHandler = require('./error'); | |||
| 
 | ||||
| const pg = require('./postgraphile'); | ||||
| 
 | ||||
| const { | ||||
| 	login, | ||||
| 	logout, | ||||
| 	signup, | ||||
| 	fetchMe, | ||||
| } = require('./auth'); | ||||
| 
 | ||||
| const { | ||||
| 	fetchScene, | ||||
| 	fetchScenes, | ||||
|  | @ -60,6 +67,12 @@ async function initServer() { | |||
| 		next(); | ||||
| 	}); | ||||
| 
 | ||||
| 	router.get('/api/session', fetchMe); | ||||
| 	router.post('/api/session', login); | ||||
| 	router.delete('/api/session', logout); | ||||
| 
 | ||||
| 	router.post('/api/users', signup); | ||||
| 
 | ||||
| 	router.get('/api/scenes', fetchScenes); | ||||
| 	router.get('/api/scenes/:releaseId', fetchScene); | ||||
| 	router.get('/api/scenes/:releaseId/poster', fetchScenePoster); | ||||
|  |  | |||