Added functional stash button on scene tiles.

This commit is contained in:
DebaucheryLibrarian 2024-03-03 02:33:35 +01:00
parent 082d4fc154
commit f56e22230b
13 changed files with 657 additions and 73 deletions

View File

@ -78,6 +78,7 @@
<VDropdown
v-if="user"
:triggers="['click']"
:prevent-overflow="true"
>
<div class="userpanel">
<img
@ -90,12 +91,22 @@
<div class="menu">
<a
:href="`/user/${user.username}`"
class="menu-header"
class="menu-header ellipsis"
>{{ user.username }}</a>
<ul class="menu-list nolist">
<li class="menu-item">
<a
:href="`/user/${user.username}`"
class="menu-button nolink"
>
<Icon icon="vcard" />
View profile
</a>
</li>
<li
class="menu-item logout"
class="menu-button menu-item logout"
@click="logout"
><Icon icon="exit2" />Log out</li>
</ul>
@ -201,9 +212,9 @@ async function logout() {
height: 2rem;
display: flex;
align-items: center;
border: solid 1px var(--shadow-weak-20);
border-radius: 1rem;
background: var(--background);
background: var(--background-dark-10);
box-shadow: inset 0 0 3px var(--shadow-weak-40);
.input {
padding: .5rem 0 .5rem 1rem;
@ -225,7 +236,10 @@ async function logout() {
}
&.focused {
/*
border: solid 1px var(--primary-light-10);
*/
box-shadow: inset 0 0 3px var(--shadow-weak-30);
.icon {
fill: var(--primary);
@ -258,25 +272,32 @@ async function logout() {
text-decoration: none;
}
.menu {
overflow: hidden;
}
.menu-header {
display: flex;
justify-content: center;
padding: .75rem 1rem;
border-bottom: solid 1px var(--shadow-weak-30);
color: var(--shadow-strong-30);
text-decoration: none;
text-align: center;
font-weight: bold;
}
.menu-item {
display: block;
}
.menu-button {
display: flex;
align-items: center;
padding: .5rem;
padding: .5rem .5rem .5rem .75rem;
.icon {
fill: var(--shadow);
margin-right: .5rem;
margin-right: .75rem;
transform: translateY(-1px);
}
&:hover {
@ -286,8 +307,6 @@ async function logout() {
}
.logout {
color: var(--error);
.icon {
fill: var(--error);
}

View File

@ -1,18 +1,34 @@
<template>
<div class="tile">
<Link
:href="`/scene/${scene.id}/${scene.slug}`"
target="_blank"
class="poster"
>
<img
v-if="scene.poster"
:src="scene.poster.isS3 ? `https://cdndev.traxxx.me/${scene.poster.thumbnail}` : `/media/${scene.poster.thumbnail}`"
:style="{ 'background-image': scene.poster.isS3 ? `url(https://cdndev.traxxx.me/${scene.poster.lazy})` : `url(/media/${scene.poster.lazy})` }"
loading="lazy"
class="thumbnail"
<div class="poster-container">
<Link
:href="`/scene/${scene.id}/${scene.slug}`"
target="_blank"
class="poster"
>
</Link>
<img
v-if="scene.poster"
:src="scene.poster.isS3 ? `https://cdndev.traxxx.me/${scene.poster.thumbnail}` : `/media/${scene.poster.thumbnail}`"
:style="{ 'background-image': scene.poster.isS3 ? `url(https://cdndev.traxxx.me/${scene.poster.lazy})` : `url(/media/${scene.poster.lazy})` }"
loading="lazy"
class="thumbnail"
>
</Link>
<Icon
v-show="favorited"
icon="heart7"
class="heart favorited"
@click.native.stop="unstash"
/>
<Icon
v-show="!favorited"
icon="heart8"
class="heart"
@click.native.stop="stash"
/>
</div>
<div class="meta">
<div class="channel">
@ -80,14 +96,42 @@
</template>
<script setup>
import { ref, inject } from 'vue';
import { format } from 'date-fns';
defineProps({
import { post, del } from '#/src/api.js';
import Icon from '../icon/icon.vue';
const props = defineProps({
scene: {
type: Object,
default: null,
},
});
const pageContext = inject('pageContext');
const user = pageContext.user;
const favorited = ref(props.scene.stashes.some((sceneStash) => sceneStash.primary));
async function stash() {
try {
favorited.value = true;
await post(`/stashes/${user.primaryStash.id}/scenes`, { sceneId: props.scene.id });
} catch (error) {
favorited.value = false;
}
}
async function unstash() {
try {
favorited.value = false;
await del(`/stashes/${user.primaryStash.id}/scenes/${props.scene.id}`);
} catch (error) {
favorited.value = true;
}
}
</script>
<style scoped>
@ -100,9 +144,17 @@ defineProps({
&:hover {
box-shadow: 0 0 3px var(--shadow-weak-20);
.heart {
fill: var(--text-light);
}
}
}
.poster-container {
position: relative;
}
.poster {
display: block;
height: 14rem;
@ -118,6 +170,26 @@ defineProps({
background-position: center;
}
.icon.heart {
width: 2rem;
height: 1.5rem;
position: absolute;
top: 0;
right: 0;
padding: .5rem .5rem 1rem 1rem;
fill: var(--highlight-strong-10);
filter: drop-shadow(0 0 3px var(--shadow));
&:hover {
cursor: pointer;
fill: var(--primary);
}
&.favorited {
fill: var(--primary);
}
}
.meta {
display: flex;
justify-content: space-between;

View File

@ -7,7 +7,7 @@ export async function onBeforeRender(pageContext) {
page: Number(pageContext.routeParams.page) || 1,
limit: Number(pageContext.urlParsed.search.limit) || 30,
aggregate: false,
});
}, pageContext.user);
return {
pageContext: {

View File

@ -9,6 +9,34 @@
<h2 class="username">{{ profile.username }}</h2>
</div>
<ul class="stashes nolist">
<li
v-for="stash in profile.stashes"
:key="`stash-${stash.id}`"
>
<a
:href="`/stash/${profile.username}/${stash.slug}`"
class="stash nolink"
>
<div class="stash-name">
{{ stash.name }}
<Icon
v-if="stash.primary"
icon="heart7"
class="primary"
/>
</div>
<div class="stash-counts">
<div class="stash-count"><Icon icon="clapboard-play" />{{ abbreviateNumber(stash.stashedScenes) }}</div>
<div class="stash-count"><Icon icon="movie" />{{ abbreviateNumber(stash.stashedMovies) }}</div>
<div class="stash-count"><Icon icon="star" />{{ abbreviateNumber(stash.stashedActors) }}</div>
</div>
</a>
</li>
</ul>
</div>
</template>
@ -17,6 +45,12 @@ import { inject } from 'vue';
const pageContext = inject('pageContext');
const profile = pageContext.pageProps.profile;
function abbreviateNumber(number) {
return number.toLocaleString('en-US', { notation: 'compact' });
}
console.log(profile.stashes);
</script>
<style scoped>
@ -38,4 +72,49 @@ const profile = pageContext.pageProps.profile;
border-radius: .25rem;
margin-right: 1rem;
}
.stashes {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
padding: 1rem;
}
.stash {
width: 100%;
background: var(--background);
box-shadow: 0 0 3px var(--shadow-weak-30);
&:hover {
box-shadow: 0 0 3px var(--shadow-weak-20);
}
}
.stash-name {
display: flex;
justify-content: space-between;
padding: .5rem;
border-bottom: solid 1px var(--shadow-weak-30);
font-weight: bold;
.icon.primary {
fill: var(--primary);
}
}
.stash-counts {
display: flex;
justify-content: space-between;
}
.stash-count {
display: inline-flex;
align-items: center;
padding: .5rem;
font-size: .9rem;
.icon {
margin-right: .5rem;
fill: var(--shadow);
}
}
</style>

View File

@ -5,7 +5,7 @@ import fs from 'fs/promises';
import { createAvatar } from '@dicebear/core';
import { shapes } from '@dicebear/collection';
import knex from './knex.js';
import { knexOwner as knex } from './knex.js';
import { curateUser, fetchUser } from './users.js';
import { HttpError } from './errors.js';
@ -38,7 +38,7 @@ async function generateAvatar(user) {
export async function login(credentials) {
if (!config.auth.login) {
throw new HttpError('Authentication is disabled', 405);
throw new HttpError('Logins are currently disabled', 405);
}
const user = await fetchUser(credentials.username.trim(), {
@ -46,6 +46,8 @@ export async function login(credentials) {
raw: true,
});
console.log('login user', user);
if (!user) {
throw new HttpError('Username or password incorrect', 401);
}
@ -60,12 +62,13 @@ export async function login(credentials) {
await generateAvatar(user);
}
// fetched the raw user for password verification, don't return directly to user
return curateUser(user);
}
export async function signup(credentials) {
if (!config.auth.signup) {
throw new HttpError('Authentication is disabled', 405);
throw new HttpError('Sign-ups are currently disabled', 405);
}
const curatedUsername = credentials.username.trim();

View File

@ -1,7 +1,16 @@
import config from 'config';
import knex from 'knex';
export default knex({
export const knexQuery = knex({
client: 'pg',
connection: config.database.query,
pool: config.database.pool,
// performance overhead, don't use asyncStackTraces in production
asyncStackTraces: process.env.NODE_ENV === 'development',
// debug: process.env.NODE_ENV === 'development',
});
export const knexOwner = knex({
client: 'pg',
connection: config.database.owner,
pool: config.database.pool,
@ -9,3 +18,5 @@ export default knex({
asyncStackTraces: process.env.NODE_ENV === 'development',
// debug: process.env.NODE_ENV === 'development',
});
export default knexQuery;

View File

@ -1,11 +1,12 @@
import config from 'config';
import knex from './knex.js';
import { knexOwner as knex } from './knex.js';
import { searchApi } from './manticore.js';
import { HttpError } from './errors.js';
import { fetchActorsById, curateActor, sortActorsByGender } from './actors.js';
import { fetchTagsById } from './tags.js';
import { fetchEntitiesById } from './entities.js';
import { curateStash } from './stashes.js';
function curateMedia(media) {
if (!media) {
@ -66,14 +67,15 @@ function curateScene(rawScene, assets) {
})),
poster: curateMedia(assets.poster),
photos: assets.photos.map((photo) => curateMedia(photo)),
stashes: assets.stashes?.map((stash) => curateStash(stash)) || [],
createdBatchId: rawScene.created_batch_id,
updatedBatchId: rawScene.updated_batch_id,
};
}
export async function fetchScenesById(sceneIds) {
const [scenes, channels, actors, directors, tags, posters, photos] = await Promise.all([
knex('releases').whereIn('id', sceneIds),
export async function fetchScenesById(sceneIds, reqUser) {
const [scenes, channels, actors, directors, tags, posters, photos, stashes] = await Promise.all([
knex('releases').whereIn('releases.id', sceneIds),
knex('releases')
.select('channels.*', 'networks.id as network_id', 'networks.slug as network_slug', 'networks.name as network_name', 'networks.type as network_type')
.whereIn('releases.id', sceneIds)
@ -92,9 +94,9 @@ export async function fetchScenesById(sceneIds) {
knex.raw('COALESCE(residence_countries.alias, residence_countries.name) as residence_country_name'),
*/
)
.whereIn('release_id', sceneIds)
.leftJoin('actors', 'actors.id', 'releases_actors.actor_id')
.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id'),
.leftJoin('actors_meta', 'actors_meta.actor_id', 'actors.id')
.whereIn('release_id', sceneIds),
/*
.leftJoin('countries as birth_countries', 'birth_countries.alpha2', 'actors_meta.birth_country_alpha2')
.leftJoin('countries as residence_countries', 'residence_countries.alpha2', 'actors_meta.residence_country_alpha2'),
@ -104,9 +106,9 @@ export async function fetchScenesById(sceneIds) {
.leftJoin('actors as directors', 'directors.id', 'releases_directors.director_id'),
knex('releases_tags')
.select('id', 'slug', 'name', 'release_id')
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
.whereNotNull('tags.id')
.whereIn('release_id', sceneIds)
.leftJoin('tags', 'tags.id', 'releases_tags.tag_id')
.orderBy('priority', 'desc'),
knex('releases_posters')
.whereIn('release_id', sceneIds)
@ -114,6 +116,12 @@ export async function fetchScenesById(sceneIds) {
knex('releases_photos')
.whereIn('release_id', sceneIds)
.leftJoin('media', 'media.id', 'releases_photos.media_id'),
reqUser
? knex('stashes_scenes')
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
.where('stashes.user_id', reqUser.id)
.whereIn('stashes_scenes.scene_id', sceneIds)
: [],
]);
return sceneIds.map((sceneId) => {
@ -129,6 +137,7 @@ export async function fetchScenesById(sceneIds) {
const sceneTags = tags.filter((tag) => tag.release_id === sceneId);
const scenePoster = posters.find((poster) => poster.release_id === sceneId);
const scenePhotos = photos.filter((photo) => photo.release_id === sceneId);
const sceneStashes = stashes.filter((stash) => stash.scene_id === sceneId);
return curateScene(scene, {
channel: sceneChannel,
@ -137,6 +146,7 @@ export async function fetchScenesById(sceneIds) {
tags: sceneTags,
poster: scenePoster,
photos: scenePhotos,
stashes: sceneStashes,
});
}).filter(Boolean);
}
@ -294,7 +304,7 @@ function countAggregations(buckets) {
return Object.fromEntries(buckets.map((bucket) => [bucket.key, { count: bucket.doc_count }]));
}
export async function fetchScenes(filters, rawOptions) {
export async function fetchScenes(filters, rawOptions, reqUser) {
const options = curateOptions(rawOptions);
const { query, sort } = buildQuery(filters);
@ -302,6 +312,8 @@ export async function fetchScenes(filters, rawOptions) {
console.log('options', options);
console.log('query', query.bool.must);
console.log('request user', reqUser);
console.time('manticore');
const result = await searchApi.search({
@ -329,8 +341,6 @@ export async function fetchScenes(filters, rawOptions) {
console.timeEnd('manticore');
console.log('hits', result.hits.hits.length);
const actorCounts = options.aggregateActors && countAggregations(result.aggregations?.actorIds?.buckets);
const tagCounts = options.aggregateTags && countAggregations(result.aggregations?.tagIds?.buckets);
const channelCounts = options.aggregateChannels && countAggregations(result.aggregations?.channelIds?.buckets);
@ -346,7 +356,7 @@ export async function fetchScenes(filters, rawOptions) {
console.timeEnd('fetch aggregations');
const sceneIds = result.hits.hits.map((hit) => Number(hit._id));
const scenes = await fetchScenesById(sceneIds);
const scenes = await fetchScenesById(sceneIds, reqUser);
return {
scenes,

227
src/stashes.js Executable file
View File

@ -0,0 +1,227 @@
import config from 'config';
import { knexOwner as knex } from './knex.js';
import { HttpError } from './errors.js';
import slugify from './utils/slugify.js';
import initLogger from './logger.js';
const logger = initLogger();
let lastActorsViewRefresh = 0;
export function curateStash(stash) {
if (!stash) {
return null;
}
const curatedStash = {
id: stash.id,
name: stash.name,
slug: stash.slug,
primary: stash.primary,
public: stash.public,
createdAt: stash.created_at,
stashedScenes: stash.stashed_scenes || null,
stashedMovies: stash.stashed_movies || null,
stashedActors: stash.stashed_actors || null,
};
return curatedStash;
}
function curateStashEntry(stash, user) {
const curatedStashEntry = {
user_id: user.id,
name: stash.name,
slug: slugify(stash.name),
public: false,
};
return curatedStashEntry;
}
export async function fetchStash(stashId, sessionUser) {
if (!sessionUser) {
throw new HttpError('You are not authenthicated', 401);
}
const stash = await knex('stashes')
.where({
id: stashId,
user_id: sessionUser.id,
})
.first();
if (!stash) {
throw new HttpError('You are not authorized to access this stash', 403);
}
return curateStash(stash);
}
export async function fetchStashes(domain, itemId, sessionUser) {
const stashes = await knex(`stashes_${domain}s`)
.select('stashes.*')
.where({
[`${domain}_id`]: itemId,
user_id: sessionUser.id,
})
.leftJoin('stashes', 'stashes.id', `stashes_${domain}s.stash_id`);
return stashes.map((stash) => curateStash(stash));
}
export async function createStash(newStash, sessionUser) {
if (!sessionUser) {
throw new HttpError('You are not authenthicated', 401);
}
try {
const stash = await knex('stashes')
.insert(curateStashEntry(newStash, sessionUser))
.returning('*');
return curateStash(stash);
} catch (error) {
if (error.routine === '_bt_check_unique') {
throw new HttpError('Stash name should be unique', 409);
}
throw error;
}
}
export async function updateStash(stashId, newStash, sessionUser) {
if (!sessionUser) {
throw new HttpError('You are not authenthicated', 401);
}
const stash = await knex('stashes')
.where({
id: stashId,
user_id: sessionUser.id,
})
.update(newStash)
.returning('*');
if (!stash) {
throw new HttpError('You are not authorized to modify this stash', 403);
}
return curateStash(stash);
}
export async function removeStash(stashId, sessionUser) {
if (!sessionUser) {
throw new HttpError('You are not authenthicated', 401);
}
const removed = await knex('stashes')
.where({
id: stashId,
user_id: sessionUser.id,
primary: false,
})
.delete();
if (removed === 0) {
throw new HttpError('Unable to remove this stash', 400);
}
}
export async function refreshActorsView() {
if (new Date() - lastActorsViewRefresh > config.stashes.viewRefreshCooldown * 60000) {
// don't refresh actors view more than once an hour
lastActorsViewRefresh = new Date();
logger.debug('Refreshing actors view');
return knex.schema.refreshMaterializedView('actors_meta');
}
logger.silly('Skipping actors view refresh');
return false;
}
export async function stashActor(actorId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_actors')
.insert({
stash_id: stash.id,
actor_id: actorId,
});
refreshActorsView();
return fetchStashes('actor', actorId, sessionUser);
}
export async function stashScene(sceneId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_scenes')
.insert({
stash_id: stash.id,
scene_id: sceneId,
});
return fetchStashes('scene', sceneId, sessionUser);
}
export async function stashMovie(movieId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_movies')
.insert({
stash_id: stash.id,
movie_id: movieId,
});
return fetchStashes('movie', movieId, sessionUser);
}
export 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, 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))
.delete();
refreshActorsView();
return fetchStashes('actor', actorId, sessionUser);
}
export async function unstashScene(sceneId, stashId, sessionUser) {
await knex
.from('stashes_scenes AS deletable')
.where('deletable.scene_id', sceneId)
.where('deletable.stash_id', stashId)
.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();
return fetchStashes('scene', sceneId, sessionUser);
}
export 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();
return fetchStashes('movie', movieId, sessionUser);
}

View File

@ -1,12 +1,13 @@
import knex from './knex.js';
// import { curateStash } from './stashes.js';
import { knexOwner as knex } from './knex.js';
import { curateStash } from './stashes.js';
import { HttpError } from './errors.js';
export function curateUser(user) {
export function curateUser(user, assets = {}) {
if (!user) {
return null;
}
const ability = [...(user.role_abilities || []), ...(user.abilities || [])];
const curatedStashes = assets.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [];
const curatedUser = {
id: user.id,
@ -14,39 +15,48 @@ export function curateUser(user) {
email: user.email,
emailVerified: user.email_verified,
identityVerified: user.identity_verified,
ability,
avatar: `/media/avatars/${user.id}_${user.username}.png`,
createdAt: user.created_at,
// stashes: user.stashes?.filter(Boolean).map((stash) => curateStash(stash)) || [],
stashes: curatedStashes,
primaryStash: curatedStashes.find((stash) => stash.primary),
};
return curatedUser;
}
function whereUser(builder, userId, options = {}) {
if (typeof userId === 'number') {
builder.where('users.id', userId);
}
if (typeof userId === 'string') {
builder.where(knex.raw('lower(users.username)'), userId.toLowerCase());
if (options.email) {
builder.orWhere(knex.raw('lower(users.email)'), userId.toLowerCase());
}
}
}
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) => {
if (typeof userId === 'number') {
builder.where('users.id', userId);
}
if (typeof userId === 'string') {
builder.where(knex.raw('lower(users.username)'), userId.toLowerCase());
if (options.email) {
builder.orWhere(knex.raw('lower(users.email)'), userId.toLowerCase());
}
}
})
.select(knex.raw('users.*, users_roles.abilities as role_abilities'))
.modify((builder) => whereUser(builder, userId, options))
.leftJoin('users_roles', 'users_roles.role', 'users.role')
.leftJoin('stashes', 'stashes.user_id', 'users.id')
.groupBy('users.id', 'users_roles.role')
.first();
if (!user) {
throw HttpError(`User '${userId}' not found`, 404);
}
if (options.raw) {
return user;
}
return curateUser(user);
const stashes = await knex('stashes')
.where('user_id', user.id)
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id');
return curateUser(user, { stashes });
}

75
src/utils/slugify.js Executable file
View File

@ -0,0 +1,75 @@
const substitutes = {
à: 'a',
á: 'a',
ä: 'a',
å: 'a',
ã: 'a',
æ: 'ae',
ç: 'c',
è: 'e',
é: 'e',
ë: 'e',
: 'e',
ì: 'i',
í: 'i',
ï: 'i',
ĩ: 'i',
ǹ: 'n',
ń: 'n',
ñ: 'n',
ò: 'o',
ó: 'o',
ö: 'o',
õ: 'o',
ø: 'o',
œ: 'oe',
ß: 'ss',
ù: 'u',
ú: 'u',
ü: 'u',
ũ: 'u',
: 'y',
ý: 'y',
ÿ: 'y',
: 'y',
};
export default function slugify(strings, delimiter = '-', {
encode = false,
removeAccents = true,
removePunctuation = false,
limit = 1000,
} = {}) {
if (!strings || (typeof strings !== 'string' && !Array.isArray(strings))) {
return strings;
}
const slugComponents = []
.concat(strings)
.filter(Boolean)
.flatMap((string) => string
.trim()
.toLowerCase()
.replace(removePunctuation && /[.,:;'"_-]/g, '')
.match(/[A-Za-zÀ-ÖØ-öø-ÿ0-9]+/g));
if (!slugComponents) {
return '';
}
const slug = slugComponents.reduce((acc, component, index) => {
const accSlug = `${acc}${index > 0 ? delimiter : ''}${component}`;
if (accSlug.length < limit) {
if (removeAccents) {
return accSlug.replace(/[à-ÿ]/g, (match) => substitutes[match] || '');
}
return accSlug;
}
return acc;
}, '');
return encode ? encodeURI(slug) : slug;
}

View File

@ -1,6 +1,5 @@
/* eslint-disable no-param-reassign */
import { login, signup } from '../auth.js';
import { fetchUser } from '../users.js';
export async function setUserApi(req, res, next) {
if (req.session.user) {
@ -27,17 +26,6 @@ export async function logoutApi(req, res) {
});
}
export async function fetchMeApi(req, res) {
if (req.session.user) {
req.session.user = await fetchUser(req.session.user.id, false, req.session.user);
res.send(req.session.user);
return;
}
res.status(401).send();
}
export async function signupApi(req, res) {
const user = await signup(req.body);

View File

@ -37,6 +37,18 @@ import {
signupApi,
} from './auth.js';
import {
createStashApi,
removeStashApi,
stashActorApi,
stashSceneApi,
stashMovieApi,
unstashActorApi,
unstashSceneApi,
unstashMovieApi,
updateStashApi,
} from './stashes.js';
import initLogger from '../logger.js';
const logger = initLogger();
@ -97,6 +109,19 @@ export default async function initServer() {
// USERS
router.post('/api/users', signupApi);
// STASHES
router.post('/api/stashes', createStashApi);
router.patch('/api/stashes/:stashId', updateStashApi);
router.delete('/api/stashes/:stashId', removeStashApi);
router.post('/api/stashes/:stashId/actors', stashActorApi);
router.post('/api/stashes/:stashId/scenes', stashSceneApi);
router.post('/api/stashes/:stashId/movies', stashMovieApi);
router.delete('/api/stashes/:stashId/actors/:actorId', unstashActorApi);
router.delete('/api/stashes/:stashId/scenes/:sceneId', unstashSceneApi);
router.delete('/api/stashes/:stashId/movies/:movieId', unstashMovieApi);
// SCENES
router.get('/api/scenes', fetchScenesApi);

65
src/web/stashes.js Executable file
View File

@ -0,0 +1,65 @@
import {
createStash,
removeStash,
stashActor,
stashScene,
stashMovie,
unstashActor,
unstashScene,
unstashMovie,
updateStash,
} from '../stashes.js';
export async function createStashApi(req, res) {
const stash = await createStash(req.body, req.session.user);
res.send(stash);
}
export async function updateStashApi(req, res) {
const stash = await updateStash(req.params.stashId, req.body, req.session.user);
res.send(stash);
}
export async function removeStashApi(req, res) {
await removeStash(req.params.stashId, req.session.user);
res.status(204).send();
}
export async function stashActorApi(req, res) {
const stashes = await stashActor(req.body.actorId, req.params.stashId, req.user);
res.send(stashes);
}
export async function stashSceneApi(req, res) {
const stashes = await stashScene(req.body.sceneId, req.params.stashId, req.user);
res.send(stashes);
}
export async function stashMovieApi(req, res) {
const stashes = await stashMovie(req.body.movieId, req.params.stashId, req.user);
res.send(stashes);
}
export async function unstashActorApi(req, res) {
const stashes = await unstashActor(req.params.actorId, req.params.stashId, req.user);
res.send(stashes);
}
export async function unstashSceneApi(req, res) {
const stashes = await unstashScene(req.params.sceneId, req.params.stashId, req.user);
res.send(stashes);
}
export async function unstashMovieApi(req, res) {
const stashes = await unstashMovie(req.params.movieId, req.params.stashId, req.user);
res.send(stashes);
}