Added various tag photos and descriptions.
|  | @ -55,14 +55,14 @@ | |||
| 					v-show="me && isStashed" | ||||
| 					icon="heart7" | ||||
| 					class="stash stashed noselect" | ||||
| 					@click="unstashScene" | ||||
| 					@click="unstashRelease" | ||||
| 				/> | ||||
| 
 | ||||
| 				<Icon | ||||
| 					v-show="me && !isStashed" | ||||
| 					icon="heart8" | ||||
| 					class="stash unstashed noselect" | ||||
| 					@click="stashScene" | ||||
| 					@click="stashRelease" | ||||
| 				/> | ||||
| 			</div> | ||||
| 
 | ||||
|  | @ -251,18 +251,20 @@ async function fetchRelease(scroll = true) { | |||
| 	} | ||||
| } | ||||
| 
 | ||||
| async function stashScene() { | ||||
| 	this.$store.dispatch('stashScene', { | ||||
| async function stashRelease() { | ||||
| 	this.$store.dispatch(this.$route.name === 'movie' ? 'stashMovie' : 'stashRelease', { | ||||
| 		sceneId: this.release.id, | ||||
| 		movieId: this.release.id, | ||||
| 		stashId: this.$store.getters.favorites.id, | ||||
| 	}); | ||||
| 
 | ||||
| 	this.fetchRelease(false); | ||||
| } | ||||
| 
 | ||||
| async function unstashScene() { | ||||
| 	this.$store.dispatch('unstashScene', { | ||||
| async function unstashRelease() { | ||||
| 	this.$store.dispatch(this.$route.name === 'movie' ? 'unstashMovie' : 'unstashRelease', { | ||||
| 		sceneId: this.release.id, | ||||
| 		movieId: this.release.id, | ||||
| 		stashId: this.$store.getters.favorites.id, | ||||
| 	}); | ||||
| 
 | ||||
|  | @ -321,8 +323,8 @@ export default { | |||
| 	mounted: fetchRelease, | ||||
| 	methods: { | ||||
| 		fetchRelease, | ||||
| 		stashScene, | ||||
| 		unstashScene, | ||||
| 		stashRelease, | ||||
| 		unstashRelease, | ||||
| 	}, | ||||
| }; | ||||
| </script> | ||||
|  |  | |||
|  | @ -138,7 +138,11 @@ function initReleasesActions(store, router) { | |||
| 		// const release = await get(`/releases/${releaseId}`);
 | ||||
| 
 | ||||
| 		const { movie } = await graphql(` | ||||
| 			query Movie($movieId: Int!) { | ||||
| 			query Movie( | ||||
| 				$movieId: Int! | ||||
| 				$hasAuth: Boolean! | ||||
| 				$userId: Int | ||||
| 			) { | ||||
| 				movie(id: $movieId) { | ||||
| 					id | ||||
| 					title | ||||
|  | @ -232,10 +236,27 @@ function initReleasesActions(store, router) { | |||
| 							hasLogo | ||||
| 						} | ||||
| 					} | ||||
| 					stashes: stashesMovies( | ||||
| 						filter: { | ||||
| 							stash: { | ||||
| 								userId: { | ||||
| 									equalTo: $userId | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					) @include(if: $hasAuth) { | ||||
| 						stash { | ||||
| 							id | ||||
| 							name | ||||
| 							slug | ||||
| 						} | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		`, {
 | ||||
| 			movieId: Number(movieId), | ||||
| 			hasAuth: !!store.state.auth.user, | ||||
| 			userId: store.state.auth.user?.id, | ||||
| 		}); | ||||
| 
 | ||||
| 		if (!movie) { | ||||
|  |  | |||
|  | @ -17,11 +17,21 @@ function initStashesActions(_store, _router) { | |||
| 		await del(`/stashes/${stashId}/scenes/${sceneId}`); | ||||
| 	} | ||||
| 
 | ||||
| 	async function stashMovie(context, { movieId, stashId }) { | ||||
| 		await post(`/stashes/${stashId}/movies`, { movieId }); | ||||
| 	} | ||||
| 
 | ||||
| 	async function unstashMovie(context, { movieId, stashId }) { | ||||
| 		await del(`/stashes/${stashId}/movies/${movieId}`); | ||||
| 	} | ||||
| 
 | ||||
| 	return { | ||||
| 		stashActor, | ||||
| 		stashScene, | ||||
| 		stashMovie, | ||||
| 		unstashActor, | ||||
| 		unstashScene, | ||||
| 		unstashMovie, | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1054,7 +1054,8 @@ exports.up = knex => Promise.resolve() | |||
| 
 | ||||
| 		table.integer('user_id') | ||||
| 			.references('id') | ||||
| 			.inTable('users'); | ||||
| 			.inTable('users') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.string('name') | ||||
| 			.notNullable(); | ||||
|  | @ -1074,27 +1075,48 @@ exports.up = knex => Promise.resolve() | |||
| 		table.integer('stash_id') | ||||
| 			.notNullable() | ||||
| 			.references('id') | ||||
| 			.inTable('stashes'); | ||||
| 			.inTable('stashes') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.integer('scene_id') | ||||
| 			.notNullable() | ||||
| 			.references('id') | ||||
| 			.inTable('releases'); | ||||
| 			.inTable('releases') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.unique(['stash_id', 'scene_id']); | ||||
| 
 | ||||
| 		table.string('comment'); | ||||
| 	})) | ||||
| 	.then(() => knex.schema.createTable('stashes_movies', (table) => { | ||||
| 		table.integer('stash_id') | ||||
| 			.notNullable() | ||||
| 			.references('id') | ||||
| 			.inTable('stashes') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.integer('movie_id') | ||||
| 			.notNullable() | ||||
| 			.references('id') | ||||
| 			.inTable('movies') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.unique(['stash_id', 'movie_id']); | ||||
| 
 | ||||
| 		table.string('comment'); | ||||
| 	})) | ||||
| 	.then(() => knex.schema.createTable('stashes_actors', (table) => { | ||||
| 		table.integer('stash_id') | ||||
| 			.notNullable() | ||||
| 			.references('id') | ||||
| 			.inTable('stashes'); | ||||
| 			.inTable('stashes') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.integer('actor_id') | ||||
| 			.notNullable() | ||||
| 			.references('id') | ||||
| 			.inTable('actors'); | ||||
| 			.inTable('actors') | ||||
| 			.onDelete('cascade'); | ||||
| 
 | ||||
| 		table.unique(['stash_id', 'actor_id']); | ||||
| 
 | ||||
|  | @ -1304,6 +1326,7 @@ exports.up = knex => Promise.resolve() | |||
| 
 | ||||
| 			ALTER TABLE stashes ENABLE ROW LEVEL SECURITY; | ||||
| 			ALTER TABLE stashes_scenes ENABLE ROW LEVEL SECURITY; | ||||
| 			ALTER TABLE stashes_movies ENABLE ROW LEVEL SECURITY; | ||||
| 			ALTER TABLE stashes_actors ENABLE ROW LEVEL SECURITY; | ||||
| 
 | ||||
| 			CREATE POLICY stashes_policy_select ON stashes FOR SELECT USING (stashes.public OR stashes.user_id = current_user_id()); | ||||
|  | @ -1319,6 +1342,14 @@ exports.up = knex => Promise.resolve() | |||
| 					AND (stashes.user_id = current_user_id() OR stashes.public) | ||||
| 				)); | ||||
| 
 | ||||
| 			CREATE POLICY stashes_policy ON stashes_movies | ||||
| 				USING (EXISTS ( | ||||
| 					SELECT * | ||||
| 					FROM stashes | ||||
| 					WHERE stashes.id = stashes_movies.stash_id | ||||
| 					AND (stashes.user_id = current_user_id() OR stashes.public) | ||||
| 				)); | ||||
| 
 | ||||
| 			CREATE POLICY stashes_policy ON stashes_actors | ||||
| 				USING (EXISTS ( | ||||
| 					SELECT * | ||||
|  | @ -1416,6 +1447,7 @@ exports.down = (knex) => { // eslint-disable-line arrow-body-style | |||
| 		DROP TABLE IF EXISTS entities CASCADE; | ||||
| 
 | ||||
| 		DROP TABLE IF EXISTS stashes_scenes CASCADE; | ||||
| 		DROP TABLE IF EXISTS stashes_movies CASCADE; | ||||
| 		DROP TABLE IF EXISTS stashes_actors CASCADE; | ||||
| 		DROP TABLE IF EXISTS stashes CASCADE; | ||||
| 
 | ||||
|  |  | |||
| After Width: | Height: | Size: 3.0 MiB | 
| After Width: | Height: | Size: 7.5 KiB | 
| After Width: | Height: | Size: 35 KiB | 
| After Width: | Height: | Size: 3.2 MiB | 
| After Width: | Height: | Size: 6.0 KiB | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 2.5 MiB | 
| After Width: | Height: | Size: 4.8 KiB | 
| After Width: | Height: | Size: 23 KiB | 
| After Width: | Height: | Size: 1.2 MiB | 
| After Width: | Height: | Size: 5.1 KiB | 
| After Width: | Height: | Size: 22 KiB | 
| After Width: | Height: | Size: 2.5 MiB | 
| After Width: | Height: | Size: 5.6 KiB | 
| After Width: | Height: | Size: 25 KiB | 
| Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 3.1 MiB | 
| Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB | 
| After Width: | Height: | Size: 2.7 MiB | 
| After Width: | Height: | Size: 3.4 MiB | 
| After Width: | Height: | Size: 1.9 MiB | 
| Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB | 
| After Width: | Height: | Size: 9.3 KiB | 
| After Width: | Height: | Size: 8.5 KiB | 
| After Width: | Height: | Size: 8.1 KiB | 
| After Width: | Height: | Size: 8.1 KiB | 
| After Width: | Height: | Size: 8.5 KiB | 
| Before Width: | Height: | Size: 495 KiB After Width: | Height: | Size: 495 KiB | 
| Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB | 
| After Width: | Height: | Size: 40 KiB | 
| After Width: | Height: | Size: 36 KiB | 
| After Width: | Height: | Size: 35 KiB | 
| After Width: | Height: | Size: 34 KiB | 
| After Width: | Height: | Size: 36 KiB | 
| After Width: | Height: | Size: 1.3 MiB | 
| After Width: | Height: | Size: 7.8 KiB | 
| After Width: | Height: | Size: 35 KiB | 
|  | @ -98,7 +98,7 @@ const tags = [ | |||
| 	{ | ||||
| 		name: 'airtight', | ||||
| 		slug: 'airtight', | ||||
| 		description: 'Stuffing one cock in her ass, one in her pussy, and one in her mouth, filling all of her penetrable holes and sealing her airtight like a figurative balloon. In other words, simultaneously getting [double penetrated](/tag/dp), and giving a [blowjob](/tag/blowjob) or getting [facefucked](/tag/facefuck). Being airtight implies being [gangbanged](/tag/gangbang).', /* eslint-disable-line max-len */ | ||||
| 		description: 'Stuffing one cock in your ass, one in your pussy, and one in your mouth, filling up all of your penetrable holes and getting sealed airtight like a figurative balloon. In other words, simultaneously getting [double penetrated](/tag/dp), and giving a [blowjob](/tag/blowjob) or getting [facefucked](/tag/facefucking). Being airtight implies being [gangbanged](/tag/gangbang).', /* eslint-disable-line max-len */ | ||||
| 		priority: 9, | ||||
| 		group: 'penetration', | ||||
| 	}, | ||||
|  | @ -242,13 +242,14 @@ const tags = [ | |||
| 		name: 'blowjob', | ||||
| 		slug: 'blowjob', | ||||
| 		priority: 5, | ||||
| 		description: 'Taking a dick in your mouth, sucking, licking and kissing it, often while giving a [handjob](/tag/handjob). You may slide it all the way [down your throat](/tag/deepthroat), or let them [fuck your face](/tag/facefucking).', | ||||
| 		group: 'oral', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'blowbang', | ||||
| 		slug: 'blowbang', | ||||
| 		priority: 9, | ||||
| 		description: 'Pleasuring a gang of three or more cocks by sucking and jerking off as many cocks as they can, often getting [facefucked](/tag/facefuck), groped and rubbed out, and followed by a [bukkake](/tag/bukkake). If they are getting fucked, it is a [gangbang](/tag/gangbang).', | ||||
| 		description: 'Pleasuring a gang of three or more cocks by sucking and jerking off as many cocks as you can, often getting [facefucked](/tag/facefucking), groped and rubbed out, and followed by a [bukkake](/tag/bukkake). If you are also getting fucked, it is a [gangbang](/tag/gangbang).', | ||||
| 		group: 'group', | ||||
| 	}, | ||||
| 	{ | ||||
|  | @ -388,13 +389,14 @@ const tags = [ | |||
| 		name: 'deepthroat', | ||||
| 		slug: 'deepthroat', | ||||
| 		priority: 6, | ||||
| 		description: 'Shoving a cock down your throat during a [blowjob](/tag/blowjob) or [facefuck](/tag/facefucking), giving them a tight sensation while showing off your skills. Without practice, their cock hitting the back of your mouth may make you [gag](/tag/gagging).', | ||||
| 		group: 'oral', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'double penetration', | ||||
| 		slug: 'dp', | ||||
| 		priority: 9, | ||||
| 		description: 'Fucking two cocks at once, with one in her ass, and one in her pussy. If she has another cock in her mouth, she is [airtight](/tag/airtight).', | ||||
| 		description: 'Getting your [ass](/tag/anal) and pussy fucked at the same time. If you take another cock in your mouth, you are [airtight](/tag/airtight).', | ||||
| 		group: 'penetration', | ||||
| 	}, | ||||
| 	{ | ||||
|  | @ -450,6 +452,7 @@ const tags = [ | |||
| 		name: 'facefucking', | ||||
| 		slug: 'facefucking', | ||||
| 		priority: 7, | ||||
| 		description: 'A [blowjob](/tag/blowjob) where you give up control, and let them fuck your mouth and [throat](/tag/deepthroat) as if it\'s your pussy.', | ||||
| 		group: 'oral', | ||||
| 	}, | ||||
| 	{ | ||||
|  | @ -487,6 +490,10 @@ const tags = [ | |||
| 		name: 'fisting DP', | ||||
| 		slug: 'fisting-dp', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'flexible', | ||||
| 		slug: 'flexible', | ||||
| 	}, | ||||
| 	{ | ||||
| 		name: 'MFF threesome', | ||||
| 		slug: 'mff', | ||||
|  |  | |||
|  | @ -650,9 +650,10 @@ const tagMedia = [ | |||
| 	['blowbang', 1, 'Nicole Black in GIO1680', 'legalporno'], | ||||
| 	['blowjob', 1, 'Kylie Page in "Stepsis Gives Soapy Handjob In Shower"', 'spyfam'], | ||||
| 	['blowjob', 4, 'Chloe Cherry in "Chloe\'s Big Anal"', 'darkx'], | ||||
| 	['blowjob', 'cecilia_lion_wefuckblackgirls', 'Cecilia Lion in "Cecilia Lion\'s Second Appearance"', 'wefuckblackgirls'], | ||||
| 	['blowjob', 0, 'Adriana Chechik in "The Dinner Party"', 'realwifestories'], | ||||
| 	['blowjob', 5, 'Kaylynn', 'mommyblowsbest'], | ||||
| 	['blowjob', 'azul_hermosa_realitykings', 'Azul Hermosa and Scott Nails in "Diva For A Day"', 'brazzers'], | ||||
| 	['blowjob', 0, 'Adriana Chechik in "The Dinner Party"', 'realwifestories'], | ||||
| 	['blowjob', 3, 'Rose Valie', 'handsonhardcore'], | ||||
| 	['blowjob', 2, 'Luna Kitsuen in "Gag Reflex"', 'evilangel'], | ||||
| 	['bondage', 0, 'Veronica Leal', 'herlimit'], | ||||
|  | @ -675,6 +676,7 @@ const tagMedia = [ | |||
| 	['cum-in-mouth', 5, 'Emma Hix in "A Big Dick"', 'darkx'], | ||||
| 	['cum-in-mouth', 4, 'Vanna Bardot and Isiah Maxwell in "Vanna Craves Isiah\'s Cock!"', 'darkx'], | ||||
| 	['cum-in-mouth', 2, 'Jaye Summers in "Double The Cum"', 'hardx'], | ||||
| 	['cum-in-mouth', 'lara_frost_legalporno', 'Lara Frost in NRX059', 'legalporno'], | ||||
| 	['cum-in-mouth', 0, 'Vina Sky and Avi Love', 'hardx'], | ||||
| 	['cum-on-boobs', 1, 'Kylie Page in "Melt In Your Mouth"', 'twistyshard'], | ||||
| 	['cum-on-boobs', 0, 'Alessandra Jane', 'private'], | ||||
|  | @ -746,6 +748,7 @@ const tagMedia = [ | |||
| 	['dp', 3, 'Hime Marie in AA047', 'legalporno'], | ||||
| 	['dp', 2, 'Megan Rain in "DP Masters 4"', 'julesjordan'], | ||||
| 	['dp', 6, 'Kira Noir', 'hardx'], | ||||
| 	['dp', 'lara_frost_legalporno', 'Lara Frost in NRX070', 'legalporno'], | ||||
| 	['dp', 5, 'Lana Rhoades in "Gangbang Me 3"', 'hardx'], | ||||
| 	['dp', 'zaawaadi_roccosiffredi', 'Zaawaadi in "My Name Is Zaawaadi"', 'roccosiffredi'], | ||||
| 	['dp', 7, 'Chloe Lamour in "DP Masters 7"', 'julesjordan'], | ||||
|  | @ -771,6 +774,7 @@ const tagMedia = [ | |||
| 	['facial', 'hope_howell_manojob', 'Hope Howell in "Super Slutty Step-Daugher"', 'manojob'], | ||||
| 	['facial', 2, 'Ashly Anderson', 'hookuphotshot'], | ||||
| 	['facial', 4, 'Kendra Heart', 'facialsforever'], | ||||
| 	['flexible', 'lara_frost_legalporno', 'Lara Frost in NRX059', 'legalporno'], | ||||
| 	['enhanced-boobs', 7, 'Charley Atwell', 'icandigirls'], | ||||
| 	['enhanced-boobs', 14, 'Rikki Six', 'dreamdolls'], | ||||
| 	['enhanced-boobs', 2, 'Gia Milana in "Hot Anal Latina"', 'hardx'], | ||||
|  | @ -787,7 +791,8 @@ const tagMedia = [ | |||
| 	['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', 'lara_frost_legalporno', 'Lara Frost in NRX059', 'legalporno'], | ||||
| 	['enhanced-boobs', 'shawna_lenee_inthecrack_3', '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'], | ||||
| 	['enhanced-boobs', 21, 'Emelie Ekström'], | ||||
|  | @ -814,10 +819,10 @@ const tagMedia = [ | |||
| 	['fisting', 0, 'Abella Danger and Karma Rx in "Neon Dreaming"', 'brazzers'], | ||||
| 	['fisting-dp', 0, 'Janice Griffith and Veronica Avluv in "The Nymphomaniac\'s Apprentice', 'theupperfloor'], | ||||
| 	['gangbang', 5, 'Carter Cruise\'s first gangbang in "Slut Puppies 9"', 'julesjordan'], | ||||
| 	['gangbang', 'poster', 'Kristen Scott in "Interracial Gangbang!"', 'julesjordan'], | ||||
| 	['gangbang', 7, 'Alexa Flexy in GL376'], | ||||
| 	['gangbang', 'kristen_scott_julesjordan', 'Kristen Scott in "Interracial Gangbang!"', 'julesjordan'], | ||||
| 	['gangbang', 'lara_frost_legalporno_1', 'Lara Frost in NRX070', 'legalporno'], | ||||
| 	['gangbang', 7, 'Alexa Flexy in GL376', 'legalporno'], | ||||
| 	['gangbang', 0, '"4 On 1 Gangbangs"', 'doghousedigital'], | ||||
| 	['gangbang', 6, 'Silvia Soprano in GIO1580', 'legalporno'], | ||||
| 	['gangbang', 4, 'Marley Brinx in "The Gangbang of Marley Brinx"', 'julesjordan'], | ||||
| 	['gangbang', 1, 'Ginger Lynn in "Gangbang Mystique", a photoset shot by Suze Randall, 1984. Depicting a woman \'airtight\' pushed the boundaries of pornography at the time.'], | ||||
| 	['gaping', 1, 'Vina Sky in "Vina Sky Does Anal"', 'hardx'], | ||||
|  | @ -914,8 +919,9 @@ const tagMedia = [ | |||
| 	['titty-fucking', 4, 'Set 5532', 'tugjobs'], | ||||
| 	['titty-fucking', 3, 'Anna Bell Peaks in "Ringing Her Bell"', 'milfvr'], | ||||
| 	['titty-fucking', 1, 'Chloe Lamour', 'ddfbusty'], | ||||
| 	['toy-anal', 1, 'Nina North and Cassidy Klein in "Nina\'s First Lesbian Anal"', 'lesbianx'], | ||||
| 	['toy-anal', 3, 'Kelly and Leona in "Sleeping Over"', 'lezcuties'], | ||||
| 	['toy-anal', 'ember_snow_jane_wilde_lesbianx', 'Ember Snow and Jane Wilde in "Ember\'s Wilde Ride"', 'lesbianx'], | ||||
| 	['toy-anal', 1, 'Nina North and Cassidy Klein in "Nina\'s First Lesbian Anal"', 'lesbianx'], | ||||
| 	['toy-anal', 2, 'Denise, Irina and Laki in "Sexy Slumber"', 'lezcuties'], | ||||
| 	['toy-anal', 0, 'Kira Noir in 1225', 'inthecrack'], | ||||
| 	['toy-dp', 1, 'Krissy Lynn and London River in "Lesbian DP Workout"', 'lesbianx'], | ||||
|  |  | |||
|  | @ -67,7 +67,7 @@ async function signup(credentials) { | |||
| 	const hashedPassword = (await scrypt(credentials.password, salt, 64)).toString('hex'); | ||||
| 	const storedPassword = `${salt}/${hashedPassword}`; | ||||
| 
 | ||||
| 	const [user] = await knex('users') | ||||
| 	const [userId] = await knex('users') | ||||
| 		.insert({ | ||||
| 			username: credentials.username, | ||||
| 			email: credentials.email, | ||||
|  | @ -76,13 +76,13 @@ async function signup(credentials) { | |||
| 		.returning('id'); | ||||
| 
 | ||||
| 	await knex('stashes').insert({ | ||||
| 		user_id: user.id, | ||||
| 		user_id: userId, | ||||
| 		name: 'Favorites', | ||||
| 		slug: 'favorites', | ||||
| 		public: false, | ||||
| 	}); | ||||
| 
 | ||||
| 	return fetchUser(user.id); | ||||
| 	return fetchUser(userId); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|  |  | |||
|  | @ -4,6 +4,10 @@ const knex = require('./knex'); | |||
| const { HttpError } = require('./errors'); | ||||
| 
 | ||||
| function curateStash(stash) { | ||||
| 	if (!stash) { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	const curatedStash = { | ||||
| 		id: stash.id, | ||||
| 		name: stash.name, | ||||
|  | @ -52,12 +56,22 @@ async function stashScene(sceneId, stashId, sessionUser) { | |||
| 		}); | ||||
| } | ||||
| 
 | ||||
| async function stashMovie(movieId, stashId, sessionUser) { | ||||
| 	const stash = await fetchStash(stashId, sessionUser); | ||||
| 
 | ||||
| 	await knex('stashes_movies') | ||||
| 		.insert({ | ||||
| 			stash_id: stash.id, | ||||
| 			movie_id: movieId, | ||||
| 		}); | ||||
| } | ||||
| 
 | ||||
| async function unstashActor(actorId, stashId, sessionUser) { | ||||
| 	await knex | ||||
| 		.from('stashes_actors AS deletable') | ||||
| 		.where('deletable.actor_id', actorId) | ||||
| 		.where('deletable.stash_id', stashId) | ||||
| 		.whereExists(knex('stashes_actors') // verify user owns this stash
 | ||||
| 		.whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security
 | ||||
| 			.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id') | ||||
| 			.where('stashes_actors.stash_id', knex.raw('deletable.stash_id')) | ||||
| 			.where('stashes.user_id', sessionUser.id)) | ||||
|  | @ -69,17 +83,31 @@ async function unstashScene(sceneId, stashId, sessionUser) { | |||
| 		.from('stashes_scenes AS deletable') | ||||
| 		.where('deletable.scene_id', sceneId) | ||||
| 		.where('deletable.stash_id', stashId) | ||||
| 		.whereExists(knex('stashes_scenes') // verify user owns this stash
 | ||||
| 		.whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security
 | ||||
| 			.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id') | ||||
| 			.where('stashes_scenes.stash_id', knex.raw('deletable.stash_id')) | ||||
| 			.where('stashes.user_id', sessionUser.id)) | ||||
| 		.delete(); | ||||
| } | ||||
| 
 | ||||
| async function unstashMovie(movieId, stashId, sessionUser) { | ||||
| 	await knex | ||||
| 		.from('stashes_movies AS deletable') | ||||
| 		.where('deletable.movie_id', movieId) | ||||
| 		.where('deletable.stash_id', stashId) | ||||
| 		.whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security
 | ||||
| 			.leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id') | ||||
| 			.where('stashes_movies.stash_id', knex.raw('deletable.stash_id')) | ||||
| 			.where('stashes.user_id', sessionUser.id)) | ||||
| 		.delete(); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	curateStash, | ||||
| 	stashActor, | ||||
| 	stashScene, | ||||
| 	stashMovie, | ||||
| 	unstashScene, | ||||
| 	unstashActor, | ||||
| 	unstashMovie, | ||||
| }; | ||||
|  |  | |||
|  | @ -18,7 +18,7 @@ function curateUser(user) { | |||
| 		identityVerified: user.identity_verified, | ||||
| 		ability, | ||||
| 		createdAt: user.created_at, | ||||
| 		stashes: user.stashes?.map(stash => curateStash(stash)) || [], | ||||
| 		stashes: user.stashes?.filter(Boolean).map(stash => curateStash(stash)) || [], | ||||
| 	}; | ||||
| 
 | ||||
| 	return curatedUser; | ||||
|  | @ -26,7 +26,7 @@ function curateUser(user) { | |||
| 
 | ||||
| async function fetchUser(userId, raw) { | ||||
| 	const user = await knex('users') | ||||
| 		.select(knex.raw('users.*, users_roles.abilities as role_abilities, json_agg(stashes) as stashes')) | ||||
| 		.select(knex.raw('users.*, users_roles.abilities as role_abilities, COALESCE(json_agg(stashes) FILTER (WHERE stashes.id IS NOT NULL), \'[]\') as stashes')) | ||||
| 		.modify((builder) => { | ||||
| 			if (typeof userId === 'number') { | ||||
| 				builder.where('users.id', userId); | ||||
|  |  | |||
|  | @ -1,10 +1,15 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const argv = require('../argv'); | ||||
| const logger = require('../logger')(__filename); | ||||
| 
 | ||||
| function errorHandler(error, req, res, _next) { | ||||
| 	logger.warn(`Failed to fulfill request to ${req.path}: ${error.message}`); | ||||
| 
 | ||||
| 	if (argv.debug) { | ||||
| 		logger.error(error); | ||||
| 	} | ||||
| 
 | ||||
| 	if (error.httpCode) { | ||||
| 		res.status(error.httpCode).send(error.message); | ||||
| 
 | ||||
|  |  | |||
|  | @ -46,8 +46,10 @@ const { | |||
| const { | ||||
| 	stashActor, | ||||
| 	stashScene, | ||||
| 	stashMovie, | ||||
| 	unstashActor, | ||||
| 	unstashScene, | ||||
| 	unstashMovie, | ||||
| } = require('./stashes'); | ||||
| 
 | ||||
| async function initServer() { | ||||
|  | @ -83,9 +85,11 @@ async function initServer() { | |||
| 
 | ||||
| 	router.post('/api/stashes/:stashId/actors', stashActor); | ||||
| 	router.post('/api/stashes/:stashId/scenes', stashScene); | ||||
| 	router.post('/api/stashes/:stashId/movies', stashMovie); | ||||
| 
 | ||||
| 	router.delete('/api/stashes/:stashId/actors/:actorId', unstashActor); | ||||
| 	router.delete('/api/stashes/:stashId/scenes/:sceneId', unstashScene); | ||||
| 	router.delete('/api/stashes/:stashId/movies/:movieId', unstashMovie); | ||||
| 
 | ||||
| 	router.get('/api/scenes', fetchScenes); | ||||
| 	router.get('/api/scenes/:releaseId', fetchScene); | ||||
|  |  | |||
|  | @ -1,6 +1,13 @@ | |||
| 'use strict'; | ||||
| 
 | ||||
| const { stashActor, stashScene, unstashActor, unstashScene } = require('../stashes'); | ||||
| const { | ||||
| 	stashActor, | ||||
| 	stashScene, | ||||
| 	stashMovie, | ||||
| 	unstashActor, | ||||
| 	unstashScene, | ||||
| 	unstashMovie, | ||||
| } = require('../stashes'); | ||||
| 
 | ||||
| async function stashActorApi(req, res) { | ||||
| 	await stashActor(req.body.actorId, req.params.stashId, req.session.user); | ||||
|  | @ -14,6 +21,12 @@ async function stashSceneApi(req, res) { | |||
| 	res.status(201).send(); | ||||
| } | ||||
| 
 | ||||
| async function stashMovieApi(req, res) { | ||||
| 	await stashMovie(req.body.movieId, req.params.stashId, req.session.user); | ||||
| 
 | ||||
| 	res.status(201).send(); | ||||
| } | ||||
| 
 | ||||
| async function unstashActorApi(req, res) { | ||||
| 	await unstashActor(req.params.actorId, req.params.stashId, req.session.user); | ||||
| 
 | ||||
|  | @ -26,9 +39,17 @@ async function unstashSceneApi(req, res) { | |||
| 	res.status(204).send(); | ||||
| } | ||||
| 
 | ||||
| async function unstashMovieApi(req, res) { | ||||
| 	await unstashMovie(req.params.movieId, req.params.stashId, req.session.user); | ||||
| 
 | ||||
| 	res.status(204).send(); | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
| 	stashActor: stashActorApi, | ||||
| 	stashScene: stashSceneApi, | ||||
| 	stashMovie: stashMovieApi, | ||||
| 	unstashActor: unstashActorApi, | ||||
| 	unstashScene: unstashSceneApi, | ||||
| 	unstashMovie: unstashMovieApi, | ||||
| }; | ||||
|  |  | |||