Added stash GraphQL mutations. Added movies to GraphQL queries. Moved key management to profile page, only for approved users.

This commit is contained in:
DebaucheryLibrarian 2025-03-31 06:14:56 +02:00
parent 09bba4fe1e
commit 1025285796
14 changed files with 721 additions and 235 deletions

View File

@ -96,8 +96,6 @@ const props = defineProps({
}, },
}); });
console.log(props.chapters);
const lastChapter = props.chapters.at(-1); const lastChapter = props.chapters.at(-1);
const duration = lastChapter.time + lastChapter.duration; const duration = lastChapter.time + lastChapter.duration;

View File

@ -1,20 +1,23 @@
<template> <template>
<div class="page"> <section class="profile-section">
<div class="manager"> <div class="section-header">
<div class="keys-header"> <h3 class="heading">API Keys</h3>
<h2 class="heading">API keys</h2>
<div class="keys-actions"> <div class="keys-actions">
<Icon <Icon
v-tooltip="'Flush all keys'" v-tooltip="'Flush all keys'"
icon="stack-cancel" icon="stack-cancel"
class="keys-flush"
@click="flushKeys" @click="flushKeys"
/> />
<button <button
class="button" class="button"
@click="createKey" @click="createKey"
>New key</button> >
<Icon icon="key" />
<span class="button-label">New key</span>
</button>
</div> </div>
</div> </div>
@ -48,6 +51,7 @@
<span class="key-actions"> <span class="key-actions">
<Icon <Icon
icon="bin" icon="bin"
class="key-remove"
@click="removeKey(key)" @click="removeKey(key)"
/> />
</span> </span>
@ -90,8 +94,7 @@
API-Key: YourSecurelyStoredApiKey12345678 API-Key: YourSecurelyStoredApiKey12345678
</code> </code>
</div> </div>
</div> </section>
</div>
</template> </template>
<script setup> <script setup>
@ -108,7 +111,7 @@ const keys = ref(pageContext.pageProps.keys);
const newKey = ref(null); const newKey = ref(null);
async function createKey() { async function createKey() {
const key = await post('/keys', null, { const key = await post('/me/keys', null, {
appendErrorMessage: true, appendErrorMessage: true,
}); });
@ -158,7 +161,6 @@ function copyKey(event) {
width: 1200px; width: 1200px;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 1rem;
} }
.keys-header { .keys-header {
@ -170,10 +172,9 @@ function copyKey(event) {
.keys-actions { .keys-actions {
display: flex; display: flex;
gap: 1rem;
align-items: center; align-items: center;
.icon { > .icon {
padding: .5rem 1rem; padding: .5rem 1rem;
} }
} }
@ -185,12 +186,18 @@ function copyKey(event) {
fill: var(--glass); fill: var(--glass);
&:hover { &:hover {
fill: var(--error);
cursor: pointer; cursor: pointer;
} }
} }
} }
.keys-flush,
.key-remove {
&:hover {
fill: var(--error);
}
}
.keys { .keys {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr)); grid-template-columns: repeat(auto-fill, minmax(25rem, 1fr));
@ -201,12 +208,14 @@ function copyKey(event) {
.key { .key {
background: var(--background); background: var(--background);
box-shadow: 0 0 3px var(--shadow-weak-30); box-shadow: 0 0 3px var(--shadow-weak-30);
border-radius: .25rem;
font-size: .9rem; font-size: .9rem;
} }
.key-row { .key-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
overflow: hidden; overflow: hidden;
} }
@ -219,16 +228,17 @@ function copyKey(event) {
.icon { .icon {
width: .9rem; width: .9rem;
height: .9rem; height: .9rem;
fill: var(--glass-strong-10); margin-right: .25rem;
fill: var(--glass);
} }
} }
.key-header .key-value { .key-header .key-value {
padding: .5rem .5rem .25rem .5rem; padding: .5rem .75rem;
} }
.key-details .key-value { .key-details .key-value {
padding: .25rem .5rem .5rem .5rem; padding: .25rem .75rem .75rem .75rem;
} }
.key-identifier { .key-identifier {
@ -238,7 +248,9 @@ function copyKey(event) {
} }
.key-actions .icon { .key-actions .icon {
padding: 0 .5rem .5rem .5rem; height: 1rem;
padding: .75rem .75rem .5rem .75rem;
overflow: hidden;
} }
.newkey { .newkey {

View File

@ -1,14 +0,0 @@
import { fetchUserKeys } from '#/src/auth.js';
export async function onBeforeRender(pageContext) {
const keys = await fetchUserKeys(pageContext.user);
return {
pageContext: {
title: 'API keys',
pageProps: {
keys,
},
},
};
}

View File

@ -49,6 +49,13 @@
class="domain nolink" class="domain nolink"
:class="{ active: section === 'revisions' && domain === 'actors' }" :class="{ active: section === 'revisions' && domain === 'actors' }"
>Actor Revisions</a> >Actor Revisions</a>
<a
v-if="profile.isIdentityVerified"
:href="`/user/${profile.username}/api`"
class="domain nolink"
:class="{ active: section === 'api' }"
>API Keys</a>
</nav> </nav>
<Stashes v-if="section === 'stashes'" /> <Stashes v-if="section === 'stashes'" />
@ -58,6 +65,10 @@
v-if="section === 'templates' && profile.id === user?.id" v-if="section === 'templates' && profile.id === user?.id"
:release="mockupRelease" :release="mockupRelease"
/> />
<ApiKeys
v-if="section === 'api'"
/>
</div> </div>
<div <div
@ -78,6 +89,7 @@ import Stashes from '#/components/stashes/stashes.vue';
import Alerts from '#/components/alerts/alerts.vue'; import Alerts from '#/components/alerts/alerts.vue';
import Summaries from '#/components/scenes/summaries.vue'; import Summaries from '#/components/scenes/summaries.vue';
import Revisions from '#/components/edit/revisions.vue'; import Revisions from '#/components/edit/revisions.vue';
import ApiKeys from '#/components/user/api-keys.vue';
const pageContext = inject('pageContext'); const pageContext = inject('pageContext');

View File

@ -1,6 +1,7 @@
import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */ import { render } from 'vike/abort'; /* eslint-disable-line import/extensions */
import { fetchUser } from '#/src/users.js'; import { fetchUser } from '#/src/users.js';
import { fetchUserKeys } from '#/src/auth.js';
import { fetchUserStashes } from '#/src/stashes.js'; import { fetchUserStashes } from '#/src/stashes.js';
import { fetchAlerts } from '#/src/alerts.js'; import { fetchAlerts } from '#/src/alerts.js';
import { fetchSceneRevisions } from '#/src/scenes.js'; import { fetchSceneRevisions } from '#/src/scenes.js';
@ -29,12 +30,15 @@ async function fetchRevisions(pageContext) {
} }
export async function onBeforeRender(pageContext) { export async function onBeforeRender(pageContext) {
const [profile, alerts, userRevisions] = await Promise.all([ const [profile, alerts, userRevisions, keys] = await Promise.all([
fetchUser(pageContext.routeParams.username, {}, pageContext.user), fetchUser(pageContext.routeParams.username, {}, pageContext.user),
pageContext.routeParams.section === 'alerts' && pageContext.routeParams.username === pageContext.user?.username pageContext.routeParams.section === 'alerts' && pageContext.routeParams.username === pageContext.user?.username
? fetchAlerts(pageContext.user) ? fetchAlerts(pageContext.user)
: [], : [],
fetchRevisions(pageContext), fetchRevisions(pageContext),
pageContext.routeParams.section === 'api'
? fetchUserKeys(pageContext.user)
: [],
]); ]);
if (!profile) { if (!profile) {
@ -49,8 +53,6 @@ export async function onBeforeRender(pageContext) {
avatars, avatars,
} = userRevisions; } = userRevisions;
console.log(userRevisions);
const stashes = await fetchUserStashes(profile.id, pageContext.user); const stashes = await fetchUserStashes(profile.id, pageContext.user);
return { return {
@ -61,6 +63,7 @@ export async function onBeforeRender(pageContext) {
stashes, stashes,
alerts, alerts,
revisions, revisions,
keys,
actors, actors,
tags, tags,
movies, movies,

View File

@ -40,6 +40,21 @@ export function curateStash(stash, assets = {}) {
return curatedStash; return curatedStash;
} }
function curateStashed(stashed) {
if (!stashed) {
return null;
}
const curatedStashed = {
id: stashed.id,
stashId: stashed.stash_id,
actorId: stashed.actor_id,
createdAt: stashed.created_at,
};
return curatedStashed;
}
function curateStashEntry(stash, user) { function curateStashEntry(stash, user) {
const curatedStashEntry = { const curatedStashEntry = {
user_id: user?.id || undefined, user_id: user?.id || undefined,
@ -57,9 +72,17 @@ function verifyStashAccess(stash, sessionUser) {
} }
} }
export async function fetchStashById(stashId, sessionUser) { export async function fetchStashById(stashIdOrSlug, sessionUser) {
const stash = await knex('stashes') const stash = await knex('stashes')
.where('id', stashId) .where((builder) => {
if (typeof stashIdOrSlug === 'number') {
builder.where('id', stashIdOrSlug);
} else {
builder
.where('slug', stashIdOrSlug)
.where('user_id', sessionUser.id);
}
})
.first(); .first();
verifyStashAccess(stash, sessionUser); verifyStashAccess(stash, sessionUser);
@ -67,8 +90,16 @@ export async function fetchStashById(stashId, sessionUser) {
return curateStash(stash); return curateStash(stash);
} }
export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUser) { export async function fetchStashByUsernameAndSlug(usernameOrId, stashSlug, sessionUser) {
const user = await knex('users').where('username', username).first(); const user = await knex('users')
.where((builder) => {
if (typeof usernameOrId === 'number') {
builder.where('id', usernameOrId);
} else {
builder.where('username', usernameOrId);
}
})
.first();
if (!user) { if (!user) {
throw new HttpError('This user does not exist.', 404); throw new HttpError('This user does not exist.', 404);
@ -86,10 +117,22 @@ export async function fetchStashByUsernameAndSlug(username, stashSlug, sessionUs
return curateStash(stash, { user }); return curateStash(stash, { user });
} }
export async function fetchUserStashes(userId, reqUser) { export async function fetchUserStashes(usernameOrId, reqUser) {
const userId = typeof usernameOrId === 'number'
? usernameOrId
: await knex('users')
.where('username', usernameOrId)
.first()
.then((user) => user?.id);
if (!userId) {
throw new HttpError(`Could not find user '${usernameOrId}'`);
}
const stashes = await knex('stashes') const stashes = await knex('stashes')
.select('stashes.*', 'stashes_meta.*') .select('stashes.*', 'stashes_meta.*')
.leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id') .leftJoin('stashes_meta', 'stashes_meta.stash_id', 'stashes.id')
.leftJoin('users', 'users.id', 'stashes.user_id')
.where('user_id', userId) .where('user_id', userId)
.modify((builder) => { .modify((builder) => {
if (userId !== reqUser?.id) { if (userId !== reqUser?.id) {
@ -172,7 +215,7 @@ export async function createStash(newStash, sessionUser) {
} }
} }
export async function updateStash(stashId, updatedStash, sessionUser) { export async function updateStash(stashIdOrSlug, updatedStash, sessionUser) {
if (!sessionUser) { if (!sessionUser) {
throw new HttpError('You are not authenthicated', 401); throw new HttpError('You are not authenthicated', 401);
} }
@ -182,11 +225,15 @@ export async function updateStash(stashId, updatedStash, sessionUser) {
} }
try { try {
const stash = await knex('stashes') const [stash] = await knex('stashes')
.where({ .where((builder) => {
id: stashId, if (typeof stashIdOrSlug === 'number') {
user_id: sessionUser.id, builder.where('id', stashIdOrSlug);
} else {
builder.where('slug', stashIdOrSlug);
}
}) })
.where('user_id', sessionUser.id)
.update(curateStashEntry(updatedStash)) .update(curateStashEntry(updatedStash))
.returning('*'); .returning('*');
@ -209,28 +256,42 @@ export async function removeStash(stashId, sessionUser) {
throw new HttpError('You are not authenthicated', 401); throw new HttpError('You are not authenthicated', 401);
} }
const removed = await knex('stashes') const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
const [removed] = await knex('stashes')
.where({ .where({
id: stashId, id: stash.id,
user_id: sessionUser.id, user_id: sessionUser.id,
primary: false, primary: false,
}) })
.delete(); .delete()
.returning('*');
if (removed === 0) { if (removed === 0) {
throw new HttpError('Unable to remove this stash', 400); throw new HttpError('Unable to remove this stash', 400);
} }
return curateStash(stash);
} }
export async function stashActor(actorId, stashId, sessionUser) { export async function stashActor(actorId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser); const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
try {
const [stashed] = await knex('stashes_actors') const [stashed] = await knex('stashes_actors')
.insert({ .insert({
stash_id: stash.id, stash_id: stash.id,
actor_id: actorId, actor_id: actorId,
}) })
.returning(['id', 'created_at']); .returning('*');
await indexApi.replace({ await indexApi.replace({
index: 'actors_stashed', index: 'actors_stashed',
@ -238,7 +299,7 @@ export async function stashActor(actorId, stashId, sessionUser) {
doc: { doc: {
actor_id: actorId, actor_id: actorId,
user_id: sessionUser.id, user_id: sessionUser.id,
stash_id: stashId, stash_id: stash.id,
created_at: Math.round(stashed.created_at.getTime() / 1000), created_at: Math.round(stashed.created_at.getTime() / 1000),
}, },
}); });
@ -247,19 +308,34 @@ export async function stashActor(actorId, stashId, sessionUser) {
refreshView('actors'); refreshView('actors');
return fetchDomainStashes('actor', actorId, sessionUser); // return fetchDomainStashes('actor', actorId, sessionUser);
return curateStashed(stashed);
} catch (error) {
if (error.routine === '_bt_check_unique') {
throw new HttpError(`Actor ${actorId} is already stashed in '${stash.name}'`, 409);
}
throw error;
}
} }
export async function unstashActor(actorId, stashId, sessionUser) { export async function unstashActor(actorId, stashId, sessionUser) {
await knex const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
const [unstashed] = await knex
.from('stashes_actors AS deletable') .from('stashes_actors AS deletable')
.where('deletable.actor_id', actorId) .where('deletable.actor_id', actorId)
.where('deletable.stash_id', stashId) .where('deletable.stash_id', stash.id)
.whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security .whereExists(knex('stashes_actors') // verify user owns this stash, complimentary to row-level security
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id') .leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
.where('stashes_actors.stash_id', knex.raw('deletable.stash_id')) .where('stashes_actors.stash_id', knex.raw('deletable.stash_id'))
.where('stashes.user_id', sessionUser.id)) .where('stashes.user_id', sessionUser.id))
.delete(); .delete()
.returning('*');
try { try {
await indexApi.callDelete({ await indexApi.callDelete({
@ -268,7 +344,7 @@ export async function unstashActor(actorId, stashId, sessionUser) {
bool: { bool: {
must: [ must: [
{ equals: { actor_id: actorId } }, { equals: { actor_id: actorId } },
{ equals: { stash_id: stashId } }, { equals: { stash_id: stash.id } },
{ equals: { user_id: sessionUser.id } }, { equals: { user_id: sessionUser.id } },
], ],
}, },
@ -278,22 +354,28 @@ export async function unstashActor(actorId, stashId, sessionUser) {
console.log(error); console.log(error);
} }
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId}`); logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed actor ${actorId} from stash ${stashId} (${stash.name})`);
refreshView('actors'); refreshView('actors');
return fetchDomainStashes('actor', actorId, sessionUser); // return fetchDomainStashes('actor', actorId, sessionUser);
return curateStashed(unstashed);
} }
export async function stashScene(sceneId, stashId, sessionUser) { export async function stashScene(sceneId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser); const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
try {
const [stashed] = await knex('stashes_scenes') const [stashed] = await knex('stashes_scenes')
.insert({ .insert({
stash_id: stash.id, stash_id: stash.id,
scene_id: sceneId, scene_id: sceneId,
}) })
.returning(['id', 'created_at']); .returning('*');
await indexApi.replace({ await indexApi.replace({
index: 'scenes_stashed', index: 'scenes_stashed',
@ -302,7 +384,7 @@ export async function stashScene(sceneId, stashId, sessionUser) {
// ...doc.replace.doc, // ...doc.replace.doc,
scene_id: sceneId, scene_id: sceneId,
user_id: sessionUser.id, user_id: sessionUser.id,
stash_id: stashId, stash_id: stash.id,
created_at: Math.round(stashed.created_at.getTime() / 1000), created_at: Math.round(stashed.created_at.getTime() / 1000),
}, },
}); });
@ -311,19 +393,34 @@ export async function stashScene(sceneId, stashId, sessionUser) {
refreshView('scenes'); refreshView('scenes');
return fetchDomainStashes('scene', sceneId, sessionUser); // return fetchDomainStashes('scene', sceneId, sessionUser);
return curateStashed(stashed);
} catch (error) {
if (error.routine === '_bt_check_unique') {
throw new HttpError(`Scene ${sceneId} is already stashed in '${stash.name}'`, 409);
}
throw error;
}
} }
export async function unstashScene(sceneId, stashId, sessionUser) { export async function unstashScene(sceneId, stashId, sessionUser) {
await knex const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
const [unstashed] = await knex
.from('stashes_scenes AS deletable') .from('stashes_scenes AS deletable')
.where('deletable.scene_id', sceneId) .where('deletable.scene_id', sceneId)
.where('deletable.stash_id', stashId) .where('deletable.stash_id', stash.id)
.whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security .whereExists(knex('stashes_scenes') // verify user owns this stash, complimentary to row-level security
.leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id') .leftJoin('stashes', 'stashes.id', 'stashes_scenes.stash_id')
.where('stashes_scenes.stash_id', knex.raw('deletable.stash_id')) .where('stashes_scenes.stash_id', knex.raw('deletable.stash_id'))
.where('stashes.user_id', sessionUser.id)) .where('stashes.user_id', sessionUser.id))
.delete(); .delete()
.returning('*');
await indexApi.callDelete({ await indexApi.callDelete({
index: 'scenes_stashed', index: 'scenes_stashed',
@ -331,29 +428,35 @@ export async function unstashScene(sceneId, stashId, sessionUser) {
bool: { bool: {
must: [ must: [
{ equals: { scene_id: sceneId } }, { equals: { scene_id: sceneId } },
{ equals: { stash_id: stashId } }, { equals: { stash_id: stash.id } },
{ equals: { user_id: sessionUser.id } }, { equals: { user_id: sessionUser.id } },
], ],
}, },
}, },
}); });
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stashId}`); logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed scene ${sceneId} from stash ${stash.id} (${stash.name})`);
refreshView('scenes'); refreshView('scenes');
return fetchDomainStashes('scene', sceneId, sessionUser); // return fetchDomainStashes('scene', sceneId, sessionUser);
return curateStashed(unstashed);
} }
export async function stashMovie(movieId, stashId, sessionUser) { export async function stashMovie(movieId, stashId, sessionUser) {
const stash = await fetchStashById(stashId, sessionUser); const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
try {
const [stashed] = await knex('stashes_movies') const [stashed] = await knex('stashes_movies')
.insert({ .insert({
stash_id: stash.id, stash_id: stash.id,
movie_id: movieId, movie_id: movieId,
}) })
.returning(['id', 'created_at']); .returning('*');
await indexApi.replace({ await indexApi.replace({
index: 'movies_stashed', index: 'movies_stashed',
@ -361,7 +464,7 @@ export async function stashMovie(movieId, stashId, sessionUser) {
doc: { doc: {
movie_id: movieId, movie_id: movieId,
user_id: sessionUser.id, user_id: sessionUser.id,
stash_id: stashId, stash_id: stash.id,
created_at: Math.round(stashed.created_at.getTime() / 1000), created_at: Math.round(stashed.created_at.getTime() / 1000),
}, },
}); });
@ -370,18 +473,33 @@ export async function stashMovie(movieId, stashId, sessionUser) {
refreshView('movies'); refreshView('movies');
return fetchDomainStashes('movie', movieId, sessionUser); // return fetchDomainStashes('movie', movieId, sessionUser);
return curateStashed(stashed);
} catch (error) {
if (error.routine === '_bt_check_unique') {
throw new HttpError(`Movie ${movieId} is already stashed in '${stash.name}'`, 409);
}
throw error;
}
} }
export async function unstashMovie(movieId, stashId, sessionUser) { export async function unstashMovie(movieId, stashId, sessionUser) {
await knex const stash = await fetchStashById(stashId, sessionUser);
if (!stash) {
throw new HttpError(`Could not find stash '${stashId}'`, 404);
}
const [unstashed] = await knex
.from('stashes_movies AS deletable') .from('stashes_movies AS deletable')
.where('deletable.movie_id', movieId) .where('deletable.movie_id', movieId)
.where('deletable.stash_id', stashId) .where('deletable.stash_id', stash.id)
.whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security .whereExists(knex('stashes_movies') // verify user owns this stash, complimentary to row-level security
.leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id') .leftJoin('stashes', 'stashes.id', 'stashes_movies.stash_id')
.where('stashes_movies.stash_id', knex.raw('deletable.stash_id')) .where('stashes_movies.stash_id', knex.raw('deletable.stash_id'))
.where('stashes.user_id', sessionUser.id)) .where('stashes.user_id', sessionUser.id))
.returning('*')
.delete(); .delete();
await indexApi.callDelete({ await indexApi.callDelete({
@ -390,18 +508,19 @@ export async function unstashMovie(movieId, stashId, sessionUser) {
bool: { bool: {
must: [ must: [
{ equals: { movie_id: movieId } }, { equals: { movie_id: movieId } },
{ equals: { stash_id: stashId } }, { equals: { stash_id: stash.id } },
{ equals: { user_id: sessionUser.id } }, { equals: { user_id: sessionUser.id } },
], ],
}, },
}, },
}); });
logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed movie ${movieId} from stash ${stashId}`); logger.verbose(`${sessionUser.username} (${sessionUser.id}) unstashed movie ${movieId} from stash ${stash.id} (${stash.name})`);
refreshView('movies'); refreshView('movies');
return fetchDomainStashes('movie', movieId, sessionUser); // return fetchDomainStashes('movie', movieId, sessionUser);
return curateStashed(unstashed);
} }
CronJob.from({ CronJob.from({

View File

@ -25,8 +25,8 @@ export function curateUser(user, _assets = {}) {
id: user.id, id: user.id,
username: user.username, username: user.username,
email: user.email, email: user.email,
emailVerified: user.email_verified, isEmailVerified: user.email_verified,
identityVerified: user.identity_verified, isIdentityVerified: user.identity_verified,
avatar: `/media/avatars/${user.id}_${user.username}.png`, avatar: `/media/avatars/${user.id}_${user.username}.png`,
role: user.role, role: user.role,
createdAt: user.created_at, createdAt: user.created_at,

View File

@ -8,6 +8,8 @@ import {
reviewActorRevision, reviewActorRevision,
} from '../actors.js'; } from '../actors.js';
import { fetchStashByUsernameAndSlug } from '../stashes.js';
export function curateActorsQuery(query) { export function curateActorsQuery(query) {
return { return {
query: query.q, query: query.q,
@ -49,6 +51,7 @@ export const actorsSchema = `
extend type Query { extend type Query {
actors( actors(
query: String query: String
stash: String
limit: Int! = 30 limit: Int! = 30
page: Int! = 1 page: Int! = 1
order: [String!] order: [String!]
@ -115,14 +118,37 @@ function curateGraphqlActor(actor) {
}; };
} }
export async function fetchActorsGraphql(query, _req) { function getOrder(query) {
if (query.order) {
return query.order;
}
if (query.query) {
return ['results', 'desc'];
}
if (query.stash) {
return ['stashed', 'desc'];
}
return ['likes', 'desc'];
}
export async function fetchActorsGraphql(query, req) {
const stash = query.stash && req.user
? await fetchStashByUsernameAndSlug(req.user.id, query.stash, req.user)
: null;
const { const {
actors, actors,
total, total,
} = await fetchActors(query, { } = await fetchActors({
query: query.query,
stashId: stash?.id,
}, {
limit: query.limit, limit: query.limit,
page: query.page, page: query.page,
order: query.order, order: getOrder(query),
aggregateCountries: false, aggregateCountries: false,
}); });

View File

@ -13,6 +13,12 @@ import {
fetchScenesByIdGraphql, fetchScenesByIdGraphql,
} from './scenes.js'; } from './scenes.js';
import {
moviesSchema,
fetchMoviesGraphql,
fetchMoviesByIdGraphql,
} from './movies.js';
import { import {
entitiesSchema, entitiesSchema,
fetchEntitiesGraphql, fetchEntitiesGraphql,
@ -25,20 +31,39 @@ import {
fetchActorsByIdGraphql, fetchActorsByIdGraphql,
} from './actors.js'; } from './actors.js';
import {
stashesSchema,
fetchUserStashesGraphql,
fetchStashGraphql,
createStashGraphql,
updateStashGraphql,
removeStashGraphql,
stashSceneGraphql,
unstashSceneGraphql,
stashActorGraphql,
unstashActorGraphql,
stashMovieGraphql,
unstashMovieGraphql,
} from './stashes.js';
import { verifyKey } from '../auth.js'; import { verifyKey } from '../auth.js';
const schema = buildSchema(` const schema = buildSchema(`
type Query { type Query {
movies( _: Boolean
limit: Int = 30 }
): ReleasesResult
type Mutation {
_: Boolean
} }
scalar Date scalar Date
${scenesSchema} ${scenesSchema}
${moviesSchema}
${actorsSchema} ${actorsSchema}
${entitiesSchema} ${entitiesSchema}
${stashesSchema}
`); `);
const DateTimeScalar = new GraphQLScalarType({ const DateTimeScalar = new GraphQLScalarType({
@ -71,6 +96,10 @@ export async function graphqlApi(req, res) {
await verifyKey(req.headers['api-user'], req.headers['api-key'], req); await verifyKey(req.headers['api-user'], req.headers['api-key'], req);
req.user = { // eslint-disable-line no-param-reassign
id: Number(req.headers['api-user']),
};
const data = await graphql({ const data = await graphql({
schema, schema,
source: req.body.query, source: req.body.query,
@ -80,9 +109,13 @@ export async function graphqlApi(req, res) {
DateScalar, DateScalar,
}, },
rootValue: { rootValue: {
// queries
scenes: async (query) => fetchScenesGraphql(query, req), scenes: async (query) => fetchScenesGraphql(query, req),
scene: async (query) => fetchScenesByIdGraphql(query, req), scene: async (query) => fetchScenesByIdGraphql(query, req),
scenesById: async (query) => fetchScenesByIdGraphql(query, req), scenesById: async (query) => fetchScenesByIdGraphql(query, req),
movies: async (query) => fetchMoviesGraphql(query, req),
movie: async (query) => fetchMoviesByIdGraphql(query, req),
moviesById: async (query) => fetchMoviesByIdGraphql(query, req),
actors: async (query) => fetchActorsGraphql(query, req), actors: async (query) => fetchActorsGraphql(query, req),
actor: async (query, args, info) => fetchActorsByIdGraphql(query, req, info), actor: async (query, args, info) => fetchActorsByIdGraphql(query, req, info),
actorsById: async (query, args, info) => fetchActorsByIdGraphql(query, req, info), actorsById: async (query, args, info) => fetchActorsByIdGraphql(query, req, info),
@ -90,6 +123,18 @@ export async function graphqlApi(req, res) {
entity: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info), entity: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info),
entitiesBySlug: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info), entitiesBySlug: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info),
entitiesById: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info), entitiesById: async (query, args, info) => fetchEntitiesByIdGraphql(query, req, info),
stashes: async (query) => fetchUserStashesGraphql(query, req),
stash: async (query) => fetchStashGraphql(query, req),
// mutation
createStash: async (query) => createStashGraphql(query, req),
updateStash: async (query) => updateStashGraphql(query, req),
removeStash: async (query) => removeStashGraphql(query, req),
stashScene: async (query) => stashSceneGraphql(query, req),
unstashScene: async (query) => unstashSceneGraphql(query, req),
stashActor: async (query) => stashActorGraphql(query, req),
unstashActor: async (query) => unstashActorGraphql(query, req),
stashMovie: async (query) => stashMovieGraphql(query, req),
unstashMovie: async (query) => unstashMovieGraphql(query, req),
}, },
}); });

View File

@ -1,8 +1,12 @@
import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */ import { stringify } from '@brillout/json-serializer/stringify'; /* eslint-disable-line import/extensions */
import { fetchMovies } from '../movies.js'; import { fetchMovies, fetchMoviesById } from '../movies.js';
import { fetchStashByUsernameAndSlug } from '../stashes.js';
import { parseActorIdentifier } from '../query.js'; import { parseActorIdentifier } from '../query.js';
import { getIdsBySlug } from '../cache.js'; import { getIdsBySlug } from '../cache.js';
import slugify from '../../utils/slugify.js';
import promiseProps from '../../utils/promise-props.js';
import { HttpError } from '../errors.js';
export async function curateMoviesQuery(query) { export async function curateMoviesQuery(query) {
return { return {
@ -41,3 +45,112 @@ export async function fetchMoviesApi(req, res) {
total, total,
})); }));
} }
export async function fetchMovieApi(req, res) {
const [movie] = await fetchMoviesById([Number(req.params.movieId)], { reqUser: req.user });
if (!movie) {
throw new HttpError(`No movie with ID ${req.params.movieId} found`, 404);
}
res.send(movie);
}
export const moviesSchema = `
extend type Query {
movies(
query: String
scope: String
entities: [String!]
actorIds: [String!]
tags: [String!]
stash: String
limit: Int! = 30
page: Int! = 1
): ReleasesResult
movie(
id: Int!
): Release
moviesById(
ids: [Int!]!
): [Release]
}
`;
function getScope(query) {
if (query.scope) {
return query.scope;
}
if (query.query) {
return 'results';
}
if (query.stash) {
return 'stashed';
}
return 'latest';
}
export async function fetchMoviesGraphql(query, req) {
const mainEntity = query.entities?.find((entity) => entity.charAt(0) !== '!');
const stash = query.stash && req.user
? await fetchStashByUsernameAndSlug(req.user.id, query.stash, req.user)
: null;
if (query.stash && !stash) {
throw new HttpError(`Could not find stash '${query.stash}'`, 404);
}
const {
tagIds,
notTagIds,
entityId,
notEntityIds,
} = await promiseProps({
tagIds: getIdsBySlug(query.tags?.filter((tag) => tag.charAt(0) !== '!'), 'tags'),
notTagIds: getIdsBySlug(query.tags?.filter((tag) => tag.charAt(0) === '!').map((tag) => tag.slice(1)).map((tag) => slugify(tag)), 'tags'),
entityId: getIdsBySlug([mainEntity], 'entities').then(([id]) => id),
notEntityIds: getIdsBySlug(query.entities?.filter((entity) => entity.charAt(0) === '!').map((entity) => entity.slice(1)), 'entities'),
});
const {
movies,
total,
} = await fetchMovies({
query: query.query, // query query query query
tagIds,
// not- currently not implemented by movies module, pass here anyway for when it is
notTagIds,
entityId,
notEntityIds,
actorIds: query.actorIds?.filter((actorId) => actorId.charAt(0) !== '!').map((actorId) => Number(actorId)),
notActorIds: query.actorIds?.filter((actorId) => actorId.charAt(0) === '!').map((actorId) => Number(actorId.slice(1))),
stashId: stash?.id,
scope: getScope(query),
isShowcased: null,
}, {
page: query.page || 1,
limit: query.limit || 30,
aggregate: false,
}, req.user);
return {
nodes: movies,
total,
};
}
export async function fetchMoviesByIdGraphql(query, req) {
const movies = await fetchMoviesById([].concat(query.id, query.ids).filter(Boolean), req.user);
if (query.ids) {
return movies;
}
return movies[0];
}

View File

@ -9,6 +9,8 @@ import {
reviewSceneRevision, reviewSceneRevision,
} from '../scenes.js'; } from '../scenes.js';
import { fetchStashByUsernameAndSlug } from '../stashes.js';
import { parseActorIdentifier } from '../query.js'; import { parseActorIdentifier } from '../query.js';
import { getIdsBySlug } from '../cache.js'; import { getIdsBySlug } from '../cache.js';
import slugify from '../../utils/slugify.js'; import slugify from '../../utils/slugify.js';
@ -87,6 +89,8 @@ export const scenesSchema = `
entities: [String!] entities: [String!]
actorIds: [String!] actorIds: [String!]
tags: [String!] tags: [String!]
movieId: Int
stash: String
limit: Int! = 30 limit: Int! = 30
page: Int! = 1 page: Int! = 1
): ReleasesResult ): ReleasesResult
@ -152,9 +156,33 @@ export const scenesSchema = `
} }
`; `;
function getScope(query) {
if (query.scope) {
return query.scope;
}
if (query.query) {
return 'results';
}
if (query.stash) {
return 'stashed';
}
return 'latest';
}
export async function fetchScenesGraphql(query, req) { export async function fetchScenesGraphql(query, req) {
const mainEntity = query.entities?.find((entity) => entity.charAt(0) !== '!'); const mainEntity = query.entities?.find((entity) => entity.charAt(0) !== '!');
const stash = query.stash && req.user
? await fetchStashByUsernameAndSlug(req.user.id, query.stash, req.user)
: null;
if (query.stash && !stash) {
throw new HttpError(`Could not find stash '${query.stash}'`, 404);
}
const { const {
tagIds, tagIds,
notTagIds, notTagIds,
@ -183,9 +211,9 @@ export async function fetchScenesGraphql(query, req) {
notEntityIds, notEntityIds,
actorIds: query.actorIds?.filter((actorId) => actorId.charAt(0) !== '!').map((actorId) => Number(actorId)), actorIds: query.actorIds?.filter((actorId) => actorId.charAt(0) !== '!').map((actorId) => Number(actorId)),
notActorIds: query.actorIds?.filter((actorId) => actorId.charAt(0) === '!').map((actorId) => Number(actorId.slice(1))), notActorIds: query.actorIds?.filter((actorId) => actorId.charAt(0) === '!').map((actorId) => Number(actorId.slice(1))),
scope: query.query && !query.scope movieId: query.movieId,
? 'results' stashId: stash?.id,
: query.scope, scope: getScope(query),
isShowcased: null, isShowcased: null,
}, { }, {
page: query.page || 1, page: query.page || 1,

View File

@ -133,7 +133,7 @@ export default async function initServer() {
// API KEYS // API KEYS
router.get('/api/me/keys', fetchUserKeysApi); router.get('/api/me/keys', fetchUserKeysApi);
router.post('/api/keys', createKeyApi); router.post('/api/me/keys', createKeyApi);
router.delete('/api/me/keys/:keyIdentifier', removeUserKeyApi); router.delete('/api/me/keys/:keyIdentifier', removeUserKeyApi);
router.delete('/api/me/keys', flushUserKeysApi); router.delete('/api/me/keys', flushUserKeysApi);

View File

@ -11,66 +11,210 @@ import {
unstashScene, unstashScene,
unstashMovie, unstashMovie,
updateStash, updateStash,
fetchStashByUsernameAndSlug,
} from '../stashes.js'; } from '../stashes.js';
export const stashesSchema = `
extend type Query {
stashes(username: String): [Stash]
stash(
username: String
stash: String!
): Stash
}
extend type Mutation {
createStash(
name: String!
isPublic: Boolean
): Stash
updateStash(
stash: String!
name: String
isPublic: Boolean
): Stash
removeStash(
stash: String!
): Stash
stashScene(
sceneId: Int!
stash: String!
): Stashed
unstashScene(
sceneId: Int!
stash: String!
): Stashed
stashActor(
actorId: Int!
stash: String!
): Stashed
unstashActor(
actorId: Int!
stash: String!
): Stashed
stashMovie(
movieId: Int!
stash: String!
): Stashed
unstashMovie(
movieId: Int!
stash: String!
): Stashed
}
type Stash {
id: Int
name: String
slug: String
isPublic: Boolean
isPrimary: Boolean
createdAt: Date
}
type Stashed {
id: Int
stashId: Int
sceneId: Int
actorId: Int
movieId: Int
serieId: Int
createdAt: Date
}
`;
export async function fetchUserStashesApi(req, res) { export async function fetchUserStashesApi(req, res) {
const stashes = await fetchUserStashes(req.user.id, req.user); const stashes = await fetchUserStashes(req.user.id, req.user);
res.send(stashes); res.send(stashes);
} }
export async function fetchUserStashesGraphql(query, req) {
const stashes = await fetchUserStashes(query.username || req.userId, req.user);
return stashes;
}
export async function fetchStashGraphql(query, req) {
const stashes = await fetchStashByUsernameAndSlug(query.username || req.userId, query.stash, req.user);
return stashes;
}
export async function createStashApi(req, res) { export async function createStashApi(req, res) {
const stash = await createStash(req.body, req.user); const stash = await createStash(req.body, req.user);
res.send(stash); res.send(stash);
} }
export async function createStashGraphql(query, req) {
const stash = await createStash(query, req.user);
return stash;
}
export async function updateStashApi(req, res) { export async function updateStashApi(req, res) {
const stash = await updateStash(Number(req.params.stashId), req.body, req.user); const stash = await updateStash(Number(req.params.stashId), req.body, req.user);
res.send(stash); res.send(stash);
} }
export async function updateStashGraphql(query, req) {
const stash = await updateStash(query.stash, query, req.user);
return stash;
}
export async function removeStashApi(req, res) { export async function removeStashApi(req, res) {
await removeStash(Number(req.params.stashId), req.user); await removeStash(Number(req.params.stashId), req.user);
res.status(204).send(); res.status(204).send();
} }
export async function removeStashGraphql(query, req) {
const removedId = await removeStash(query.stash, req.user);
return removedId;
}
export async function stashActorApi(req, res) { export async function stashActorApi(req, res) {
const stashes = await stashActor(req.body.actorId, Number(req.params.stashId), req.user); const stashed = await stashActor(req.body.actorId, Number(req.params.stashId), req.user);
res.send(stashes); res.send(stashed);
} }
export async function stashSceneApi(req, res) { export async function stashActorGraphql(query, req) {
const stashes = await stashScene(req.body.sceneId, Number(req.params.stashId), req.user); const stashed = await stashActor(query.actorId, query.stash, req.user);
res.send(stashes); return stashed;
}
export async function stashMovieApi(req, res) {
const stashes = await stashMovie(req.body.movieId, Number(req.params.stashId), req.user);
res.send(stashes);
} }
export async function unstashActorApi(req, res) { export async function unstashActorApi(req, res) {
const stashes = await unstashActor(Number(req.params.actorId), Number(req.params.stashId), req.user); const unstashed = await unstashActor(Number(req.params.actorId), Number(req.params.stashId), req.user);
res.send(stashes); res.send(unstashed);
}
export async function unstashActorGraphql(query, req) {
const stashes = await unstashActor(query.actorId, query.stash, req.user);
return stashes;
}
export async function stashSceneApi(req, res) {
const stashed = await stashScene(req.body.sceneId, Number(req.params.stashId), req.user);
res.send(stashed);
}
export async function stashSceneGraphql(query, req) {
const stashed = await stashScene(query.sceneId, query.stash, req.user);
return stashed;
} }
export async function unstashSceneApi(req, res) { export async function unstashSceneApi(req, res) {
const stashes = await unstashScene(Number(req.params.sceneId), Number(req.params.stashId), req.user); const unstashed = await unstashScene(Number(req.params.sceneId), Number(req.params.stashId), req.user);
res.send(stashes); res.send(unstashed);
}
export async function unstashSceneGraphql(query, req) {
const unstashed = await unstashScene(query.sceneId, query.stash, req.user);
return unstashed;
}
export async function stashMovieApi(req, res) {
const stashed = await stashMovie(req.body.movieId, Number(req.params.stashId), req.user);
res.send(stashed);
}
export async function stashMovieGraphql(query, req) {
const stashed = await stashMovie(query.movieId, query.stash, req.user);
return stashed;
} }
export async function unstashMovieApi(req, res) { export async function unstashMovieApi(req, res) {
const stashes = await unstashMovie(Number(req.params.movieId), Number(req.params.stashId), req.user); const unstashed = await unstashMovie(Number(req.params.movieId), Number(req.params.stashId), req.user);
res.send(stashes); res.send(unstashed);
}
export async function unstashMovieGraphql(query, req) {
const unstashed = await unstashMovie(query.movieId, query.stash, req.user);
return unstashed;
} }
export const router = Router(); export const router = Router();

2
static

@ -1 +1 @@
Subproject commit 75b0bfabb314627472f24310557c7f2c31d109b1 Subproject commit 4982084fd86168758424ad9f343d729eee97a7e2