Added functional stash button on scene tiles.
This commit is contained in:
parent
082d4fc154
commit
f56e22230b
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
13
src/knex.js
13
src/knex.js
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
54
src/users.js
54
src/users.js
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
Loading…
Reference in New Issue