Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.idea/
/webapp/coverage/
/archive/

/webapp/.nuxt-storybook
/webapp/storybook-static
git-crypt-key
23 changes: 23 additions & 0 deletions backend/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,29 @@ const resolvers = {
finally {
await session.close();
}
},
delete: async(parent, args, context, info) => {
const { title } = args;
const { token, authService, driver } = context;
const session = driver.session();

const { records: authorRecords} = await session.readTransaction((tx) =>
tx.run("MATCH (u: User)-[:AUTHORED]->(p: Post {title: $title}) RETURN u.id as uId", { title })
);

const authorId = authorRecords[0].get('uId');
if (authorId !== token.uId) {
throw new UserInputError("This is not your post!");
}

return await delegateToSchema({
schema: neo4jSchema,
operation: 'mutation',
fieldName: 'DeletePost',
args,
context,
info
})
}
},
}
Expand Down
3 changes: 2 additions & 1 deletion backend/src/security/permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const permissions = shield({
signup: allow,
login: allow,// not(isAuthenticated, new Error("Already logged in. Redirect to home page.")),
write: isAuthenticated,
upvote: isAuthenticated
upvote: isAuthenticated,
delete: isAuthenticated
}
}, {
allowExternalErrors: true,
Expand Down
1 change: 1 addition & 0 deletions backend/src/typeDefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const typeDefs = gql`
signup(name: String!, email: String!, password: String!): String
write(post: PostInput!): Post
upvote(title: ID!): Post
delete(title: ID!): Post
}

input PostInput {
Expand Down
3 changes: 3 additions & 0 deletions webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pids
*.seed
*.pid.lock

.nuxt-storybook
storybook-static

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

Expand Down
24 changes: 8 additions & 16 deletions webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@

## Build Setup

```bash
# install dependencies
$ npm install

# serve with hot reload at localhost:3000
$ npm run dev

# build for production and launch server
$ npm run build
$ npm run start

# generate static project
$ npm run generate
```

For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org).
Just run`npm run build && npm run generate`.
Comment thread
MaykAkifovski marked this conversation as resolved.
The first command will create a `.nuxt` directory that will bundle everything
that is to be deployed on the target server.

Our `nuxt.config.js` clearly states that the application is to be statically hosted.
Therefore, our bundle needs to contain the pre-rendered pages.
The `generate` command does this by creating a `dist` directory containing
our 'compiled' static `.html` and `.js` files which can now be served by a simple webserver.
Comment thread
MaykAkifovski marked this conversation as resolved.
96 changes: 96 additions & 0 deletions webapp/components/login-form/LoginForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<template>
<v-form v-model="isFormValid">
<v-container>
<v-row>
<v-col>
<v-text-field
v-model="credentials.email"
id="email"
:rules="nameRules"
:counter="64"
label="User name"
required
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
v-model="credentials.password"
:rules="pwdRules"
label="Password"
id="password"
required
type="password"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-btn
v-on:submit.prevent
id="login"
class="mr-4"
:disabled="!isFormValid"
@click="login"
>
Login
</v-btn>
</v-col>
</v-row>
</v-container>
</v-form>
</template>

<script>

import gql from 'graphql-tag';

export default {
name: 'LoginForm',
data: () => ({
isFormValid: false,
credentials: {
"email": '',
'password': '',
},
}),
methods: {
async login() {
const creds = this.credentials;
try {
const { data: { login } } = await this.$apollo.mutate({
mutation: gql`mutation login($email: String!, $password: String!) {
login(email: $email, password: $password)
}`,
variables: creds,
});

await this.$apolloHelpers.onLogin(login); // Stores the token in a cookie called apollo-token
this.$store.commit('setPrincipal', login);
}
catch (e) {
}
}
},
computed: {
nameRules() {
return [
name => {
return !!name || 'User name is required!'
},
name => {
return name.length <= 64 || 'User name cannot exceed 64 chars!'
}
]
},
pwdRules() {
return [
pwd => !!pwd || 'Password is required',
pwd => pwd.length >= 8 || 'Password must be at least 8 chars!'
]
},
Comment thread
MaykAkifovski marked this conversation as resolved.

},
}
</script>
40 changes: 40 additions & 0 deletions webapp/components/menu/Menu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<div>
<v-app-bar
dense
light
>
<v-toolbar-title>Blog</v-toolbar-title>
<v-spacer></v-spacer>

<v-btn v-if="logged" @click="logout">Logout</v-btn>
<v-btn v-else><nuxt-link to="/login">Login</nuxt-link></v-btn>
</v-app-bar>
</div>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
name: 'Menu',
methods: {
async logout() {
await this.$apolloHelpers.onLogout(); // Deletes the cookie containing the token
this.$store.commit('removePrincipal');
},
},
computed: {
...mapGetters({
logged: 'isAuthenticated',
})
}
}
</script>
Comment thread
MaykAkifovski marked this conversation as resolved.

<style scoped>
button a {
color: inherit;
text-decoration: none;
}
</style>
39 changes: 33 additions & 6 deletions webapp/components/news-item/NewsItem.vue
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
<template>
<div>
<h1>{{ newsItem.title }} {{ (newsItem.votes) }}</h1>
<button class="upvote" @click="incrementVotes">Upvote</button>
<button class="downvote" @click="decrementVotes">Downvote</button>
<button class="remove" @click="removeMe">Remove</button>
<button v-if="isAuth" class="upvote" @click="incrementVotes">Upvote</button>
<button v-if="isAuth" class="downvote" @click="decrementVotes">Downvote</button>
<button v-if="ownsPost" class="remove" @click="removeMe">Remove</button>
Comment thread
MaykAkifovski marked this conversation as resolved.
</div>
</template>

<script>
import gql from 'graphql-tag';
import { mapGetters } from 'vuex';
export default {
name: "NewsItem",
props: {
newsItem: {
title: {type: String, required: true},
votes: {type: String, required: true},
author: {type: Object, required: true}
}
},
methods: {
incrementVotes() {
this.$emit('updateNews', { ...this.newsItem, votes: (this.newsItem.votes + 1)})
async incrementVotes() {
const mutation = gql`
mutation ($title: ID!) {
upvote(title: $title) {
votes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request the ID here and let vue-apollo do all the work to update the Post object! 💪

}
}
`;
try {
const { data: {upvote}} = await this.$apollo.mutate({
mutation,
variables: { title: this.newsItem.title }
})
this.$emit('updateNews', {...this.newsItem, votes: upvote.votes});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could remove all code to update things that have been previously cached. vue-apollo will do it for you.

}
catch (e) { console.log(e.message); }
},
decrementVotes() {
this.$emit('updateNews', { ...this.newsItem, votes: (this.newsItem.votes - 1)})
},
removeMe() {
this.$emit('removeNews', this.newsItem)
}
},
computed: {
...mapGetters({
isAuth: 'isAuthenticated',
user: 'getPrincipal'
}),
ownsPost() {
return this.isAuth && this.user.id === this.newsItem.author.id;
}
}
}

</script>

<style scoped>

</style>
</style>
Loading