Added favorites button to actor page.

This commit is contained in:
DebaucheryLibrarian 2021-03-15 03:30:47 +01:00
parent e371e9725a
commit 77b40817f2
27 changed files with 466 additions and 169 deletions

View File

@ -33,6 +33,20 @@
:actor="actor" :actor="actor"
class="header-social" class="header-social"
/> />
<Icon
v-show="me && isStashed"
icon="heart7"
class="stash stashed noselect"
@click="unstashActor"
/>
<Icon
v-show="me && !isStashed"
icon="heart8"
class="stash unstashed noselect"
@click="stashActor"
/>
</div> </div>
<div class="content-inner actor-inner"> <div class="content-inner actor-inner">
@ -54,13 +68,6 @@
> >
</a> </a>
<Expand
v-if="bioExpanded"
:expanded="bioExpanded"
class="expand expand-light"
@expand="(state) => bioExpanded = state"
/>
<ul class="bio nolist"> <ul class="bio nolist">
<li <li
v-if="actor.realName" v-if="actor.realName"
@ -384,7 +391,7 @@ import Scroll from '../scroll/scroll.vue';
import Gender from './gender.vue'; import Gender from './gender.vue';
import Social from './social.vue'; import Social from './social.vue';
async function fetchActor() { async function fetchActor(scroll = true) {
const { actor, releases, totalCount } = await this.$store.dispatch('fetchActorById', { const { actor, releases, totalCount } = await this.$store.dispatch('fetchActorById', {
actorId: Number(this.$route.params.actorId), actorId: Number(this.$route.params.actorId),
limit: this.limit, limit: this.limit,
@ -396,11 +403,37 @@ async function fetchActor() {
this.releases = releases; this.releases = releases;
this.totalCount = totalCount; this.totalCount = totalCount;
if (this.$refs.filter) { if (this.$refs.filter && scroll) {
this.$refs.filter.$el.scrollIntoView(); this.$refs.filter.$el.scrollIntoView();
} }
} }
async function stashActor() {
this.$store.dispatch('stashActor', {
actorId: this.actor.id,
stashId: this.$store.getters.favorites.id,
});
this.fetchActor(false);
}
async function unstashActor() {
this.$store.dispatch('unstashActor', {
actorId: this.actor.id,
stashId: this.$store.getters.favorites.id,
});
this.fetchActor(false);
}
function me() {
return this.$store.state.auth.user;
}
function isStashed() {
return this.actor.stashes?.length > 0;
}
function sfw() { function sfw() {
return this.$store.state.ui.sfw; return this.$store.state.ui.sfw;
} }
@ -447,6 +480,8 @@ export default {
}; };
}, },
computed: { computed: {
isStashed,
me,
sfw, sfw,
showAlbum, showAlbum,
}, },
@ -457,6 +492,8 @@ export default {
mounted, mounted,
methods: { methods: {
fetchActor, fetchActor,
stashActor,
unstashActor,
}, },
}; };
</script> </script>
@ -477,11 +514,10 @@ export default {
align-items: center; align-items: center;
color: var(--lighten-extreme); color: var(--lighten-extreme);
background: var(--profile); background: var(--profile);
padding: .5rem 1rem;
} }
.header-name { .header-name {
padding: 0; padding: .5rem 1rem;
margin: 0; margin: 0;
display: inline-flex; display: inline-flex;
justify-content: space-between; justify-content: space-between;
@ -491,7 +527,7 @@ export default {
.header-gender { .header-gender {
display: inline-block; display: inline-block;
margin: 0 0 0 .5rem; margin: 0 0 0 .5rem;
transform: translate(0, .1rem); transform: translate(0, .125rem);
} }
.header-social { .header-social {
@ -731,6 +767,22 @@ export default {
border-bottom: solid 1px var(--shadow-hint); border-bottom: solid 1px var(--shadow-hint);
} }
.stash.icon {
width: 1.5rem;
height: 1.5rem;
padding: 0 1rem;
fill: var(--lighten);
&.stashed {
fill: var(--primary);
}
&:hover {
fill: var(--primary);
cursor: pointer;
}
}
@media(max-width: $breakpoint4) { @media(max-width: $breakpoint4) {
.descriptions-container { .descriptions-container {
display: none; display: none;
@ -795,8 +847,16 @@ export default {
} }
.header-name { .header-name {
flex-grow: 1; flex-grow: 1;
font-size: 1.3rem; font-size: 1.3rem;
padding: .5rem .5rem .5rem 1rem;
} }
.stash.icon {
width: 1.25rem;
height: 1.25rem;
padding: 0 1rem 0 .25rem;
transform: translate(0, -.1rem);
}
} }
</style> </style>

View File

@ -81,8 +81,8 @@
@input="(range) => updateValue('age', range, false)" @input="(range) => updateValue('age', range, false)"
@change="(range) => updateValue('age', range, true)" @change="(range) => updateValue('age', range, true)"
> >
<template v-slot:start><Icon icon="flower" /></template> <template v-slot:start><Icon icon="leaf" /></template>
<template v-slot:end><Icon icon="pipe" /></template> <template v-slot:end><Icon icon="tree3" /></template>
</RangeFilter> </RangeFilter>
<div class="filter-section"> <div class="filter-section">

View File

@ -21,8 +21,10 @@
<script> <script>
async function search() { async function search() {
this.$router.push({ name: 'search', query: { q: this.query } }); if (this.query) {
this.$emit('search'); this.$router.push({ name: 'search', query: { q: this.query } });
this.$emit('search');
}
} }
function searching(to) { function searching(to) {

View File

@ -8,31 +8,32 @@
</div> </div>
<section <section
v-if="stashes.length > 0" v-if="user.stashes?.length > 0"
class="section" class="section"
> >
<h3 class="heading">Stashes</h3> <h3 class="heading">Stashes</h3>
<ul class="stashes nolist"> <ul class="stashes nolist">
<li <li
v-for="stash in stashes" v-for="stash in user.stashes"
:key="stash.id" :key="stash.id"
class="stash"
> >
<h4 class="stash-name">{{ stash.name }}</h4> <h4 class="stash-name">{{ stash.name }}</h4>
<ul class="stash nolist actors"> <ul class="stash-section stash-scenes nolist">
<li
v-for="item in stash.actors"
:key="item.id"
><Actor :actor="item.actor" /></li>
</ul>
<ul class="stash nolist scenes">
<li <li
v-for="item in stash.scenes" v-for="item in stash.scenes"
:key="item.id" :key="item.id"
><Scene :release="item.scene" /></li> ><Scene :release="item.scene" /></li>
</ul> </ul>
<ul class="stash-section stash-actors nolist">
<li
v-for="item in stash.actors"
:key="item.id"
><Actor :actor="item.actor" /></li>
</ul>
</li> </li>
</ul> </ul>
</section> </section>
@ -44,8 +45,8 @@ import Actor from '../actors/tile.vue';
import Scene from '../releases/scene-tile.vue'; import Scene from '../releases/scene-tile.vue';
async function mounted() { async function mounted() {
this.user = await this.$store.dispatch('fetchMe'); this.user = await this.$store.dispatch('fetchUser', this.$route.params.username);
this.stashes = await this.$store.dispatch('fetchUserStashes', this.user.id); this.pageTitle = this.user?.username;
} }
export default { export default {
@ -58,7 +59,7 @@ export default {
user: this.$route.params.username === this.$store.state.auth.user?.username user: this.$route.params.username === this.$store.state.auth.user?.username
? this.$store.state.auth.user ? this.$store.state.auth.user
: null, : null,
stashes: [], pageTitle: null,
}; };
}, },
mounted, mounted,
@ -67,7 +68,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.header { .header {
padding: 1rem; padding: .5rem 1rem;
background: var(--profile); background: var(--profile);
} }
@ -87,27 +88,38 @@ export default {
} }
.stash { .stash {
background: var(--background);
margin: 0 0 1rem 0; margin: 0 0 1rem 0;
box-shadow: 0 0 3px var(--shadow-weak);
} }
.stash-name { .stash-name {
color: var(--shadow-strong); color: var(--shadow-strong);
margin: 0 0 1rem 0; padding: 1rem .5rem 0 .5rem;
margin: 0;
} }
.actors { .stash-section {
padding: 1rem .5rem;
&:not(:last-child) {
border-bottom: solid 1px var(--shadow-hint);
}
}
.stash-actors,
.stash-scenes {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-gap: .5rem;
flex-grow: 1; flex-grow: 1;
flex-wrap: wrap;
}
.scenes {
width: 100%;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
grid-gap: .5rem; grid-gap: .5rem;
box-sizing: border-box; box-sizing: border-box;
} }
.stash-actors {
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}
.stash-scenes {
grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr));
}
</style> </style>

View File

@ -5,6 +5,7 @@
color: var(--shadow-strong); color: var(--shadow-strong);
background: var(--background); background: var(--background);
font-size: 1rem; font-size: 1rem;
font-family: inherit;
&:focus { &:focus {
border: solid 1px var(--primary); border: solid 1px var(--primary);

View File

@ -12,7 +12,7 @@ $breakpoint4: 1500px;
*/ */
--primary: #f28; --primary: #f28;
--primary-strong: #f90071; --primary-strong: #f90071;
--primary-faded: #ff4e9f; --primary-faded: #ffcce4;
--text-dark: #222; --text-dark: #222;
--text-light: #fff; --text-light: #fff;

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>leaf2</title>
<path d="M15.802 2.102c-1.73-1.311-4.393-2.094-7.124-2.094-3.377 0-6.129 1.179-7.549 3.235-0.667 0.965-1.036 2.109-1.097 3.398-0.065 1.375 0.225 2.925 0.859 4.606-0.586 1.428-0.904 2.898-0.904 4.254 0 0.276 0.224 0.5 0.5 0.5s0.5-0.224 0.5-0.5c0-1.198 0.293-2.535 0.818-3.835 1.472 0.272 2.712 0.405 3.776 0.405 1.839 0 3.146-0.398 4.115-1.252 0.868-0.765 1.347-1.794 1.854-2.882 0.774-1.663 1.651-3.547 4.198-5.002 0.146-0.083 0.24-0.234 0.251-0.402s-0.063-0.329-0.197-0.431zM10.644 7.515c-0.481 1.034-0.897 1.927-1.608 2.554-0.776 0.684-1.873 1.002-3.454 1.002-0.945 0-2.047-0.113-3.351-0.345 0.238-0.476 0.507-0.941 0.804-1.386 0.692-1.036 1.505-1.931 2.417-2.661 0.984-0.788 2.059-1.36 3.193-1.7 0.264-0.079 0.415-0.358 0.335-0.623s-0.358-0.415-0.623-0.335c-1.257 0.377-2.445 1.009-3.53 1.878-0.991 0.794-1.874 1.765-2.623 2.886-0.252 0.378-0.485 0.767-0.698 1.163-0.36-1.185-0.52-2.279-0.474-3.261 0.052-1.099 0.361-2.067 0.921-2.876 0.636-0.92 1.583-1.633 2.816-2.119 1.134-0.447 2.487-0.684 3.911-0.684 2.172 0 4.357 0.555 5.9 1.475-2.314 1.551-3.206 3.467-3.935 5.032z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>tree2</title>
<path d="M13.887 13.182l-2.387-3.182h1c0 0 0.001 0 0.001 0 0.276 0 0.5-0.224 0.5-0.5 0-0.121-0.043-0.231-0.114-0.318l-2.387-3.182h1c0.192 0 0.367-0.11 0.451-0.283s0.060-0.379-0.060-0.529l-4-5c-0.095-0.119-0.239-0.188-0.39-0.188s-0.296 0.069-0.39 0.188l-4 5c-0.12 0.15-0.143 0.356-0.060 0.529s0.258 0.283 0.451 0.283h1l-2.4 3.2c-0.114 0.152-0.132 0.354-0.047 0.524s0.258 0.276 0.447 0.276h1l-2.4 3.2c-0.114 0.152-0.132 0.354-0.047 0.524s0.258 0.276 0.447 0.276h4.5v1.5c0 0.276 0.224 0.5 0.5 0.5h2c0.276 0 0.5-0.224 0.5-0.5v-1.5h4.5c0 0 0 0 0.001 0 0.276 0 0.5-0.224 0.5-0.5 0-0.121-0.043-0.231-0.114-0.318zM8 15h-1v-1h1v1zM2.5 13l2.4-3.2c0.114-0.152 0.132-0.354 0.047-0.524s-0.258-0.276-0.447-0.276h-1l2.4-3.2c0.114-0.152 0.132-0.354 0.047-0.524s-0.258-0.276-0.447-0.276h-0.96l2.96-3.7 2.96 3.7h-0.96c-0.189 0-0.363 0.107-0.447 0.276s-0.066 0.372 0.047 0.524l2.4 3.2h-1c-0.189 0-0.363 0.107-0.447 0.276s-0.066 0.372 0.047 0.524l2.4 3.2h-10z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>tree3</title>
<path d="M11.852 3.354c-0.508-1.928-2.266-3.354-4.352-3.354s-3.844 1.426-4.352 3.354c-1.891 0.754-3.148 2.596-3.148 4.646 0 2.757 2.243 5 5 5 0.34 0 0.674-0.035 1-0.101v2.601c0 0.276 0.224 0.5 0.5 0.5h2c0.276 0 0.5-0.224 0.5-0.5v-2.601c0.327 0.066 0.661 0.101 1 0.101 2.757 0 5-2.243 5-5 0-2.060-1.254-3.892-3.148-4.646z"></path>
</svg>

After

Width:  |  Height:  |  Size: 489 B

View File

@ -0,0 +1,5 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>tree4</title>
<path d="M11.852 3.354c-0.508-1.928-2.266-3.354-4.352-3.354s-3.844 1.426-4.352 3.354c-1.891 0.754-3.148 2.596-3.148 4.646 0 2.757 2.243 5 5 5h1v2.5c0 0.276 0.224 0.5 0.5 0.5h2c0.276 0 0.5-0.224 0.5-0.5v-2.5h1c2.757 0 5-2.243 5-5 0-2.060-1.254-3.892-3.148-4.646zM8 15h-1v-2h1v2zM10 12h-5c-2.206 0-4-1.794-4-4 0-1.444 0.781-2.76 2.001-3.465 0.003 0.369 0.050 0.735 0.141 1.090 0.058 0.226 0.261 0.375 0.484 0.375 0.041 0 0.083-0.005 0.125-0.016 0.267-0.069 0.428-0.341 0.36-0.609-0.073-0.284-0.11-0.579-0.11-0.875 0-1.93 1.57-3.5 3.5-3.5s3.5 1.57 3.5 3.5c0 0.297-0.037 0.591-0.11 0.875-0.069 0.267 0.092 0.54 0.36 0.609s0.54-0.092 0.609-0.36c0.091-0.355 0.139-0.722 0.141-1.091 1.222 0.704 2.001 2.014 2.001 3.466 0 2.206-1.794 4-4 4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View File

@ -24,6 +24,8 @@ function initActorActions(store, router) {
const { actor } = await graphql(` const { actor } = await graphql(`
query Actor( query Actor(
$actorId: Int! $actorId: Int!
$userId: Int,
$hasAuth: Boolean!,
$limit:Int = 10, $limit:Int = 10,
$offset:Int = 0, $offset:Int = 0,
$after:Datetime = "1900-01-01", $after:Datetime = "1900-01-01",
@ -236,6 +238,21 @@ function initActorActions(store, router) {
} }
totalCount totalCount
} }
stashes: stashesActors(
filter: {
stash: {
userId: {
equalTo: $userId
}
}
}
) @include(if: $hasAuth) {
stash {
id
name
slug
}
}
} }
} }
`, { `, {
@ -253,6 +270,8 @@ function initActorActions(store, router) {
includedEntities: getIncludedEntities(router), includedEntities: getIncludedEntities(router),
includedActors: getIncludedActors(router), includedActors: getIncludedActors(router),
mode, mode,
hasAuth: !!store.state.auth.user,
userId: store.state.auth.user?.id,
}); });
if (!actor) { if (!actor) {

View File

@ -10,10 +10,16 @@ async function get(endpoint, query = {}) {
credentials: 'same-origin', credentials: 'same-origin',
}); });
if (res.ok) { const contentTypes = res.headers.get('content-type');
if (res.ok && contentTypes?.includes('application/json')) {
return res.json(); return res.json();
} }
if (res.ok) {
return null;
}
const errorMsg = await res.text(); const errorMsg = await res.text();
throw new Error(errorMsg); throw new Error(errorMsg);
@ -30,10 +36,16 @@ async function post(endpoint, data) {
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (res.ok) { const contentTypes = res.headers.get('content-type');
if (res.ok && contentTypes?.includes('application/json')) {
return res.json(); return res.json();
} }
if (res.ok) {
return null;
}
const errorMsg = await res.text(); const errorMsg = await res.text();
throw new Error(errorMsg); throw new Error(errorMsg);

View File

@ -2,11 +2,16 @@ import { get, post, del } from '../api';
function initAuthActions(_store, _router) { function initAuthActions(_store, _router) {
async function fetchMe({ commit }) { async function fetchMe({ commit }) {
const user = await get('/session'); try {
const user = await get('/session');
commit('setUser', user); commit('setUser', user);
return user; return user;
} catch (error) {
// continue as guest
return null;
}
} }
async function login({ commit }, credentials) { async function login({ commit }, credentials) {

View File

@ -1,11 +1,13 @@
import state from './state'; import state from './state';
import mutations from './mutations'; import mutations from './mutations';
import getters from './getters';
import actions from './actions'; import actions from './actions';
function initAuthStore(store, router) { function initAuthStore(store, router) {
return { return {
state, state,
mutations, mutations,
getters,
actions: actions(store, router), actions: actions(store, router),
}; };
} }

View File

@ -0,0 +1,8 @@
function favoritesStash(state) {
return state.user.stashes.find(stash => stash.slug === 'favorites');
}
module.exports = {
favoritesStash,
favorites: favoritesStash,
};

View File

@ -56,6 +56,8 @@ function curateActor(actor, release) {
curatedActor.aliasFor = curateActor(curatedActor.aliasFor); curatedActor.aliasFor = curateActor(curatedActor.aliasFor);
} }
curatedActor.stashes = actor.stashes?.map(stash => stash.stash || stash) || [];
return curatedActor; return curatedActor;
} }
@ -126,9 +128,7 @@ function curateTag(tag) {
} }
function curateStash(stash) { function curateStash(stash) {
const curatedStash = { const curatedStash = stash;
...stash,
};
if (stash.scenes) { if (stash.scenes) {
curatedStash.scenes = stash.scenes.map(item => ({ curatedStash.scenes = stash.scenes.map(item => ({
@ -147,10 +147,21 @@ function curateStash(stash) {
return curatedStash; return curatedStash;
} }
function curateUser(user) {
const curatedUser = user;
if (user.stashes) {
curatedUser.stashes = user.stashes.map(stash => curateStash(stash));
}
return curatedUser;
}
export { export {
curateActor, curateActor,
curateEntity, curateEntity,
curateRelease, curateRelease,
curateTag, curateTag,
curateStash, curateStash,
curateUser,
}; };

View File

@ -64,8 +64,8 @@ async function init() {
return `${path}/${filename}`; return `${path}/${filename}`;
} }
initUiObservers(store, router); await initAuthObservers(store, router);
initAuthObservers(store, router); await initUiObservers(store, router);
if (window.env.sfw) { if (window.env.sfw) {
store.dispatch('setSfw', true); store.dispatch('setSfw', true);

View File

@ -1,101 +1,17 @@
import { graphql } from '../api'; import { post, del } from '../api';
import { curateStash } from '../curate';
function initStashesActions(_store, _router) { function initStashesActions(_store, _router) {
async function fetchUserStashes(context, userId) { async function stashActor(context, { actorId, stashId }) {
const { stashes } = await graphql(` await post(`/stashes/${stashId}/actors`, { actorId });
query Stashes( }
$userId: Int!
) {
stashes(
filter: {
userId: {
equalTo: $userId
}
}
) {
id
name
actors: stashesActors {
comment
actor {
id
name
slug
gender
age
ageFromBirth
dateOfBirth
birthCity
birthState
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
avatar: avatarMedia {
id
path
thumbnail
lazy
}
}
}
scenes: stashesScenes {
comment
scene {
id
title
slug
url
date
actors: releasesActors {
actor {
id
name
slug
}
}
tags: releasesTags {
tag {
id
name
slug
}
}
entity {
id
name
slug
independent
parent {
id
name
slug
independent
}
}
poster: releasesPosterByReleaseId {
media {
path
thumbnail
lazy
isS3
}
}
}
}
}
}
`, {
userId,
});
return stashes.map(stash => curateStash(stash)); async function unstashActor(context, { actorId, stashId }) {
await del(`/stashes/${stashId}/actors/${actorId}`);
} }
return { return {
fetchUserStashes, stashActor,
unstashActor,
}; };
} }

View File

@ -1,8 +1,95 @@
import { get } from '../api'; import { graphql } from '../api';
function initUsersActions(_store, _router) { function initUsersActions(_store, _router) {
async function fetchUser(context, username) { async function fetchUser(context, username) {
const user = await get(`/users/${username}`); const { user } = await graphql(`
query User(
$username: String!
) {
user: userByUsername(username: $username) {
id
role
username
stashes {
id
name
slug
public
actors: stashesActors {
comment
actor {
id
name
slug
gender
age
ageFromBirth
dateOfBirth
birthCity
birthState
birthCountry: countryByBirthCountryAlpha2 {
alpha2
name
alias
}
avatar: avatarMedia {
id
path
thumbnail
lazy
}
}
}
scenes: stashesScenes {
comment
scene {
id
title
slug
url
date
actors: releasesActors {
actor {
id
name
slug
}
}
tags: releasesTags {
tag {
id
name
slug
}
}
entity {
id
name
slug
independent
parent {
id
name
slug
independent
}
}
poster: releasesPosterByReleaseId {
media {
path
thumbnail
lazy
isS3
}
}
}
}
}
}
}
`, {
username,
});
return user; return user;
} }

View File

@ -1081,6 +1081,8 @@ exports.up = knex => Promise.resolve()
.references('id') .references('id')
.inTable('releases'); .inTable('releases');
table.unique(['stash_id', 'scene_id']);
table.string('comment'); table.string('comment');
})) }))
.then(() => knex.schema.createTable('stashes_actors', (table) => { .then(() => knex.schema.createTable('stashes_actors', (table) => {
@ -1094,6 +1096,8 @@ exports.up = knex => Promise.resolve()
.references('id') .references('id')
.inTable('actors'); .inTable('actors');
table.unique(['stash_id', 'actor_id']);
table.string('comment'); table.string('comment');
})) }))
// SEARCH // SEARCH
@ -1295,21 +1299,24 @@ exports.up = knex => Promise.resolve()
GRANT ALL ON ALL TABLES IN SCHEMA public TO :visitor; GRANT ALL ON ALL TABLES IN SCHEMA public TO :visitor;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :visitor; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO :visitor;
REVOKE ALL ON users FROM :visitor;
GRANT SELECT (id, username, role, identity_verified, created_at) ON users TO :visitor;
ALTER TABLE stashes ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes ENABLE ROW LEVEL SECURITY;
ALTER TABLE stashes_scenes ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes_scenes ENABLE ROW LEVEL SECURITY;
ALTER TABLE stashes_actors ENABLE ROW LEVEL SECURITY; ALTER TABLE stashes_actors ENABLE ROW LEVEL SECURITY;
CREATE POLICY stashes_policy_select ON stashes FOR SELECT USING (stashes.user_id = current_user_id()); CREATE POLICY stashes_policy_select ON stashes FOR SELECT USING (stashes.public OR stashes.user_id = current_user_id());
CREATE POLICY stashes_policy_update ON stashes FOR UPDATE USING (stashes.user_id = current_user_id()); CREATE POLICY stashes_policy_update ON stashes FOR UPDATE USING (stashes.public OR stashes.user_id = current_user_id());
CREATE POLICY stashes_policy_delete ON stashes FOR DELETE USING (stashes.user_id = current_user_id()); CREATE POLICY stashes_policy_delete ON stashes FOR DELETE USING (stashes.public OR stashes.user_id = current_user_id());
CREATE POLICY stashes_policy_insert ON stashes FOR INSERT WITH CHECK(true); CREATE POLICY stashes_policy_insert ON stashes FOR INSERT WITH CHECK (true);
CREATE POLICY stashes_policy ON stashes_scenes CREATE POLICY stashes_policy ON stashes_scenes
USING (EXISTS ( USING (EXISTS (
SELECT * SELECT *
FROM stashes FROM stashes
WHERE stashes.id = stashes_scenes.stash_id WHERE stashes.id = stashes_scenes.stash_id
AND stashes.user_id = current_user_id() AND (stashes.user_id = current_user_id() OR stashes.public)
)); ));
CREATE POLICY stashes_policy ON stashes_actors CREATE POLICY stashes_policy ON stashes_actors
@ -1317,7 +1324,7 @@ exports.up = knex => Promise.resolve()
SELECT * SELECT *
FROM stashes FROM stashes
WHERE stashes.id = stashes_actors.stash_id WHERE stashes.id = stashes_actors.stash_id
AND stashes.user_id = current_user_id() AND (stashes.user_id = current_user_id() OR stashes.public)
)); ));
`, { `, {
visitor: knex.raw(config.database.query.user), visitor: knex.raw(config.database.query.user),
@ -1328,8 +1335,10 @@ exports.up = knex => Promise.resolve()
.then(() => { // eslint-disable-line arrow-body-style .then(() => { // eslint-disable-line arrow-body-style
// allow vim fold // allow vim fold
return knex.raw(` return knex.raw(`
COMMENT ON TABLE users IS E'@omit'; COMMENT ON COLUMN users.password IS E'@omit';
COMMENT ON TABLE users_roles IS E'@omit'; COMMENT ON COLUMN users.email IS E'@omit';
COMMENT ON COLUMN users.email_verified IS E'@omit';
COMMENT ON COLUMN users.abilities IS E'@omit';
COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many'; COMMENT ON COLUMN actors.height IS E'@omit read,update,create,delete,all,many';
COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many'; COMMENT ON COLUMN actors.weight IS E'@omit read,update,create,delete,all,many';

View File

@ -4,7 +4,7 @@ const util = require('util');
const crypto = require('crypto'); const crypto = require('crypto');
const knex = require('./knex'); const knex = require('./knex');
const { curateUser } = require('./users'); const { curateUser, fetchUser } = require('./users');
const { HttpError } = require('./errors'); const { HttpError } = require('./errors');
const scrypt = util.promisify(crypto.scrypt); const scrypt = util.promisify(crypto.scrypt);
@ -21,12 +21,7 @@ async function verifyPassword(password, storedPassword) {
} }
async function login(credentials) { async function login(credentials) {
const user = await knex('users') const user = await fetchUser(credentials.username, true);
.select('users.*', 'users_roles.abilities as role_abilities')
.where('username', credentials.username)
.orWhere('email', credentials.username)
.leftJoin('users_roles', 'users_roles.role', 'users.role')
.first();
if (!user) { if (!user) {
throw new HttpError('Username or password incorrect', 401); throw new HttpError('Username or password incorrect', 401);
@ -69,7 +64,7 @@ async function signup(credentials) {
email: credentials.email, email: credentials.email,
password: storedPassword, password: storedPassword,
}) })
.returning('*'); .returning('id');
await knex('stashes').insert({ await knex('stashes').insert({
user_id: user.id, user_id: user.id,
@ -78,7 +73,7 @@ async function signup(credentials) {
public: false, public: false,
}); });
return curateUser(user); return fetchUser(user.id);
} }
module.exports = { module.exports = {

73
src/stashes.js Normal file
View File

@ -0,0 +1,73 @@
'use strict';
const knex = require('./knex');
const { HttpError } = require('./errors');
function curateStash(stash) {
const curatedStash = {
id: stash.id,
name: stash.name,
slug: stash.slug,
};
return curatedStash;
}
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 modify this stash', 403);
}
return stash;
}
async function stashActor(actorId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_actors')
.insert({
stash_id: stash.id,
actor_id: actorId,
});
}
async function stashScene(sceneId, stashId, sessionUser) {
const stash = await fetchStash(stashId, sessionUser);
await knex('stashes_scenes')
.insert({
stash_id: stash.id,
actor_id: sceneId,
});
}
async function unstashActor(actorId, stashId, sessionUser) {
await knex
.from('stashes_actors')
.whereIn('stashes_actors.id', knex('stashes_actors')
.select('stashes_actors.id')
.leftJoin('stashes', 'stashes.id', 'stashes_actors.stash_id')
.where('stashes.user_id', sessionUser.id) // verify user owns this stash
.where('stashes_actors.actor_id', actorId)
.where('stashes_actors.stash_id', stashId))
.delete();
}
module.exports = {
curateStash,
stashActor,
stashScene,
// unstashScene,
unstashActor,
};

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const knex = require('./knex'); const knex = require('./knex');
const { curateStash } = require('./stashes');
function curateUser(user) { function curateUser(user) {
if (!user) { if (!user) {
@ -17,20 +18,35 @@ function curateUser(user) {
identityVerified: user.identity_verified, identityVerified: user.identity_verified,
ability, ability,
createdAt: user.created_at, createdAt: user.created_at,
stashes: user.stashes?.map(stash => curateStash(stash)) || [],
}; };
return curatedUser; return curatedUser;
} }
async function fetchUser(userId) { async function fetchUser(userId, raw) {
const user = await knex('users') const user = await knex('users')
.select('users.*', 'users_roles.abilities as role_abilities') .select(knex.raw('users.*, users_roles.abilities as role_abilities, json_agg(stashes) as stashes'))
.where('id', userId) .modify((builder) => {
.orWhere('username', userId) if (typeof userId === 'number') {
.orWhere('email', userId) builder.where('users.id', userId);
}
if (typeof userId === 'string') {
builder
.where('users.username', userId)
.orWhere('users.email', userId);
}
})
.leftJoin('users_roles', 'users_roles.role', 'users.role') .leftJoin('users_roles', 'users_roles.role', 'users.role')
.leftJoin('stashes', 'stashes.user_id', 'users.id')
.groupBy('users.id', 'users_roles.role')
.first(); .first();
if (raw) {
return user;
}
return curateUser(user); return curateUser(user);
} }

View File

@ -34,6 +34,7 @@ async function fetchMeApi(req, res) {
async function signupApi(req, res) { async function signupApi(req, res) {
const user = await signup(req.body); const user = await signup(req.body);
req.session.user = user;
res.send(user); res.send(user);
} }

View File

@ -23,6 +23,7 @@ module.exports = postgraphile(
'public', 'public',
{ {
// watchPg: true, // watchPg: true,
disableDefaultMutations: true,
dynamicJson: true, dynamicJson: true,
graphiql: true, graphiql: true,
enhanceGraphiql: true, enhanceGraphiql: true,

View File

@ -43,6 +43,13 @@ const {
fetchTags, fetchTags,
} = require('./tags'); } = require('./tags');
const {
stashActor,
stashScene,
unstashActor,
unstashScene,
} = require('./stashes');
async function initServer() { async function initServer() {
const app = express(); const app = express();
const router = Router(); const router = Router();
@ -74,6 +81,12 @@ async function initServer() {
router.post('/api/users', signup); router.post('/api/users', signup);
router.post('/api/stashes/:stashId/actors', stashActor);
router.post('/api/stashes/:stashId/scenes', stashScene);
router.delete('/api/stashes/:stashId/actors/:actorId', unstashActor);
router.delete('/api/stashes/:stashId/scenes/:sceneId', unstashScene);
router.get('/api/scenes', fetchScenes); router.get('/api/scenes', fetchScenes);
router.get('/api/scenes/:releaseId', fetchScene); router.get('/api/scenes/:releaseId', fetchScene);
router.get('/api/scenes/:releaseId/poster', fetchScenePoster); router.get('/api/scenes/:releaseId/poster', fetchScenePoster);

34
src/web/stashes.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
const { stashActor, stashScene, unstashActor, unstashScene } = require('../stashes');
async function stashActorApi(req, res) {
await stashActor(req.body.actorId, req.params.stashId, req.session.user);
res.status(201).send();
}
async function stashSceneApi(req, res) {
await stashScene(req.body.sceneId, req.params.stashId, req.session.user);
res.status(201).send();
}
async function unstashActorApi(req, res) {
await unstashActor(req.params.actorId, req.params.stashId, req.session.user);
res.status(204).send();
}
async function unstashSceneApi(req, res) {
await unstashScene(req.params.sceneId, req.params.stashId, req.session.user);
res.status(204).send();
}
module.exports = {
stashActor: stashActorApi,
stashScene: stashSceneApi,
unstashActor: unstashActorApi,
unstashScene: unstashSceneApi,
};