Skip to content

Commit

Permalink
Add sample single page application working with REST API backend #108 #…
Browse files Browse the repository at this point in the history
  • Loading branch information
simukappu committed Jan 19, 2020
1 parent 008b201 commit 30545c4
Show file tree
Hide file tree
Showing 37 changed files with 1,845 additions and 8 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ build-iPhoneSimulator/
/gemfiles/vendor/bundle
/lib/bundler/man/

# Ignore webpacker files
/spec/rails_app/node_modules
/spec/rails_app/yarn.lock
/spec/rails_app/yarn-error.log
/spec/rails_app/public/packs
/spec/rails_app/public/packs-test

# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# Gemfile.lock
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ group :test do
gem 'coveralls', require: false
end

gem 'webpacker', groups: [:production, :development]
gem 'rack-cors', groups: [:production, :development]
gem 'dotenv-rails', groups: [:development, :test]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ The deployed demo application is included in this gem's source code as a test ap
REST API reference as OpenAPI Specification is published in SwaggerHub here:
* **https://app.swaggerhub.com/apis/simukappu/activity-notification/**

You can see sample single page application using [Vue.js](https://vuejs.org) as a part of example Rails application here:
* **https://activity-notification-example.herokuapp.com/spa/**

This sample application works with *activity_notification* REST API backend.


## Table of Contents

Expand Down
4 changes: 3 additions & 1 deletion bin/deploy_on_heroku.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ CURRENT_BRANCH=`git symbolic-ref --short HEAD`
git checkout -b $HEROKU_DEPLOYMENT_BRANCH
bundle install
sed -i "" -e "s/^\/Gemfile.lock/# \/Gemfile.lock/g" .gitignore
cp spec/rails_app/bin/webpack* bin/
git add .gitignore
git add Gemfile.lock
git commit -m "Add Gemfile.lock"
git add bin/webpack*
git commit -m "Add Gemfile.lock and webpack"
git push heroku ${HEROKU_DEPLOYMENT_BRANCH}:master --force
git checkout $CURRENT_BRANCH
git branch -D $HEROKU_DEPLOYMENT_BRANCH
2 changes: 2 additions & 0 deletions docs/Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@ Then, *activity_notification* uses *[ActivityNotification::SubscriptionsApiContr

When you want to use REST API backend integrated with Devise authentication, see [REST API backend with Devise Token Auth](#rest-api-backend-with-devise-token-auth).

You can see [sample single page application](/spec/rails_app/app/javascript/) using [Vue.js](https://vuejs.org) as a part of example Rails application. This sample application works with *activity_notification* REST API backend.

#### API reference as OpenAPI Specification

*activity_notification* provides API reference as [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification).
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"engines": {
"yarn": "1.x"
},
"scripts": {
"postinstall": "cd ./spec/rails_app && yarn && yarn install --check-files"
}
}
21 changes: 21 additions & 0 deletions spec/rails_app/app/controllers/admins_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class AdminsController < ApplicationController
before_action :set_admin, only: [:show]

# GET /users
def index
render json: {
users: Admin.all.as_json(include: :user)
}
end

# GET /users/:id
def show
render json: @admin.as_json(include: :user)
end

private
# Use callbacks to share common setup or constraints between actions.
def set_admin
@admin = Admin.find(params[:id])
end
end
7 changes: 7 additions & 0 deletions spec/rails_app/app/controllers/spa_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class SpaController < ApplicationController

# GET /spa
def index
end

end
21 changes: 21 additions & 0 deletions spec/rails_app/app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class UsersController < ApplicationController
before_action :set_user, only: [:show]

# GET /users
def index
render json: {
users: User.all
}
end

# GET /users/:id
def show
render json: @user
end

private
# Use callbacks to share common setup or constraints between actions.
def set_user
@user = User.find(params[:id])
end
end
93 changes: 93 additions & 0 deletions spec/rails_app/app/javascript/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<template>
<div>
<router-view />
</div>
</template>

<script>
import Vue from 'vue'
import VueRouter from 'vue-router'
import VueMoment from 'vue-moment'
import moment from 'moment-timezone'
import VuePluralize from 'vue-pluralize'
import axios from 'axios'
import authStore from "./store/auth"
import Top from './components/Top.vue'
import DeviseTokenAuth from './components/DeviseTokenAuth.vue'
import NotificationsIndex from './components/notifications/Index.vue'
import SubscriptionsIndex from './components/subscriptions/Index.vue'
const router = new VueRouter({
routes: [
{ path: '/', component: Top },
{ path: '/login', component: DeviseTokenAuth },
{ path: '/logout', component: DeviseTokenAuth, props: { isLogout: true } },
{
path: '/notifications',
name: 'AuthenticatedUserNotificationsIndex',
component: NotificationsIndex,
props: () => ({ target: authStore.getters.currentUser }),
meta: { requiresAuth: true }
},
{
path: '/admins/notifications',
name: 'AuthenticatedAdminNotificationsIndex',
component: NotificationsIndex,
props: () => ({ target: authStore.getters.currentUser.admin, targetApiPath: 'admins' }),
meta: { requiresAuth: true }
},
{
path: '/:target_type/:target_id/notifications',
name: 'UnauthenticatedTargetNotificationsIndex',
component: NotificationsIndex,
props : true
},
{
path: '/subscriptions',
name: 'AuthenticatedUserSubscriptionsIndex',
component: SubscriptionsIndex,
props: () => ({ target: authStore.getters.currentUser }),
meta: { requiresAuth: true }
},
{
path: '/admins/subscriptions',
name: 'AuthenticatedAdminSubscriptionsIndex',
component: SubscriptionsIndex,
props: () => ({ target: authStore.getters.currentUser.admin, targetApiPath: 'admins' }),
meta: { requiresAuth: true }
},
{
path: '/:target_type/:target_id/subscriptions',
name: 'UnauthenticatedTargetSubscriptionsIndex',
component: SubscriptionsIndex,
props : true
}
]
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth) && !authStore.getters.userSignedIn) {
next({ path: '/login', query: { redirect: to.fullPath }});
} else {
next();
}
})
if (authStore.getters.userSignedIn) {
for (var authHeader of Object.keys(authStore.getters.authHeaders)) {
axios.defaults.headers.common[authHeader] = authStore.getters.authHeaders[authHeader];
}
}
Vue.use(VueRouter)
Vue.use(VueMoment, { moment })
Vue.use(VuePluralize)
export default {
name: 'App',
router
}
</script>

<style scoped>
</style>
83 changes: 83 additions & 0 deletions spec/rails_app/app/javascript/components/DeviseTokenAuth.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<template>
<div id="login">
<h2>Log in</h2>
<form class="new_user" @submit.prevent="login">
<div class="field">
<label for="user_email">Email</label><br />
<input v-model="loginParams.email" autofocus="autofocus" autocomplete="email" type="email" value="" name="user[email]" id="user_email" />
</div>
<div class="field">
<label for="user_password">Password</label><br />
<input v-model="loginParams.password" autocomplete="current-password" type="password" name="user[password]" id="user_password" />
</div>
<div class="actions">
<input type="submit" name="commit" value="Log in" data-disable-with="Log in" />
</div>
</form>
</div>
</template>

<script>
import axios from 'axios'
import authStore from "../store/auth"
export default {
name: 'DeviseTokenAuth',
props: {
isLogout: {
type: Boolean,
default: false
}
},
data () {
return {
loginParams: {
email: "",
password: ""
}
}
},
mounted () {
if (this.isLogout) {
this.logout();
}
},
methods: {
login () {
axios
.post('/auth/sign_in', { email: this.loginParams.email, password: this.loginParams.password })
.then(response => {
if (response.status == 200) {
let authHeaders = {};
for (let authHeader of ['access-token', 'client', 'uid']) {
authHeaders[authHeader] = response.headers[authHeader];
axios.defaults.headers.common[authHeader] = authHeaders[authHeader];
}
authStore.commit('signIn', { user: response.data.data, authHeaders: authHeaders });
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect);
} else {
this.$router.push('/');
}
}
})
.catch (error => {
console.log("Authentication failed");
if (error.response.status == 401) {
this.$router.go({path: this.$router.currentRoute.path});
}
})
},
logout () {
for (var authHeader of Object.keys(authStore.getters.authHeaders)) {
delete axios.defaults.headers.common[authHeader];
}
authStore.commit('signOut');
this.$router.push('/');
}
}
}
</script>

<style scoped>
</style>
99 changes: 99 additions & 0 deletions spec/rails_app/app/javascript/components/Top.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<template>
<div>
<section>
<h1>Authentecated User</h1>
<div class="list_wrapper">
<div class="list_image"></div>
<div class="list_description_wrapper">
<div class="list_description">
<div v-if="userSignedIn">
<span>{{ currentUser.name }}</span> · {{ currentUser.email }} · <router-link v-bind:to="{ path: '/logout' }">Logout</router-link><br>
</div>
<div v-else>
<span>Not logged in</span> · <router-link v-bind:to="{ path: '/login' }">Login</router-link><br>
</div>
<router-link v-bind:to="{ name : 'AuthenticatedUserNotificationsIndex' }">Notifications</router-link> /
<router-link v-bind:to="{ name : 'AuthenticatedUserSubscriptionsIndex' }">Subscriptions</router-link>
</div>
</div>
</div>
</section>

<section>
<h1>Listing Users</h1>
<div v-for="user in users" :key="`${user.id}`" class="list_wrapper">
<div class="list_image"></div>
<div class="list_description_wrapper">
<p class="list_description">
<span>{{ user.name }}</span> · {{ user.email }}<br>
<router-link v-bind:to="{ name : 'UnauthenticatedTargetNotificationsIndex', params : { target_type: 'users', target_id: user.id, target: user }}">Notifications</router-link> /
<router-link v-bind:to="{ name : 'UnauthenticatedTargetSubscriptionsIndex', params : { target_type: 'users', target_id: user.id, target: user }}">Subscriptions</router-link>
</p>
</div>
</div>
</section>

<section>
<h1>Authentecated User as Admin</h1>
<div class="list_wrapper">
<div class="list_image"></div>
<div class="list_description_wrapper">
<div class="list_description">
<div v-if="userSignedIn">
<span>{{ currentUser.name }}</span> · {{ currentUser.email }} · <span v-if="currentUser.admin">(admin)</span><span v-else>(not admin)</span><br>
</div>
<div v-else>
<span>Not logged in</span> · <router-link v-bind:to="{ path: '/login' }">Login</router-link><br>
</div>
<router-link v-bind:to="{ name : 'AuthenticatedAdminNotificationsIndex' }">Notifications</router-link> /
<router-link v-bind:to="{ name : 'AuthenticatedAdminSubscriptionsIndex' }">Subscriptions</router-link>
</div>
</div>
</div>
</section>

<section>
<h1>Listing Admins</h1>
<div v-for="admin in admins" :key="`${admin.id}`" class="list_wrapper">
<div class="list_image"></div>
<div class="list_description_wrapper">
<p class="list_description">
<span>{{ admin.user.name }}</span> · {{ admin.user.email }}<br>
<router-link v-bind:to="{ name : 'UnauthenticatedTargetNotificationsIndex', params : { target_type: 'admins', target_id: admin.id, target: admin }}">Notifications</router-link> /
<router-link v-bind:to="{ name : 'UnauthenticatedTargetSubscriptionsIndex', params : { target_type: 'admins', target_id: admin.id, target: admin }}">Subscriptions</router-link>
</p>
</div>
</div>
</section>
</div>
</template>

<script>
import axios from 'axios'
import authStore from "../store/auth"
export default {
name: 'Top',
data () {
return {
userSignedIn: authStore.getters.userSignedIn,
currentUser: authStore.getters.currentUser,
users: [],
admins: []
}
},
mounted () {
axios
.get('/users')
.then(response => {
this.users = response.data.users;
this.admins = this.users
.filter(user => user.admin)
.map(user => Object.assign(Object.create(user.admin), { user: user }))
})
}
}
</script>

<style scoped>
</style>
Loading

0 comments on commit 30545c4

Please sign in to comment.