Initial commit
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.nuxt
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM cypress/base:10 as builder
|
||||
|
||||
# If only we could use globs in COPY or ADD. But alas, we can not.
|
||||
COPY ./package.json /build/package.json
|
||||
COPY ./yarn.lock /build/yarn.lock
|
||||
|
||||
WORKDIR /build
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY ./ ./
|
||||
WORKDIR /build/autopublish-app
|
||||
RUN yarn run lint && yarn run build
|
||||
RUN yarn start:ci & yarn wait && yarn test:cypress
|
||||
RUN yarn run generate
|
||||
|
||||
FROM nginx
|
||||
MAINTAINER Joakim Olsson <joakim@unbound.se>
|
||||
|
||||
COPY --from=builder /build/dist/ /usr/share/nginx/html/
|
||||
COPY --from=builder /build/nginx-conf/default.conf /etc/nginx/conf.d/
|
||||
@@ -0,0 +1,25 @@
|
||||
body,
|
||||
html {
|
||||
font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.t-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-fade-in {
|
||||
animation: fade-in 350ms ease-in-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div v-lazy:background-image="image" class="image" @click="onClick">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
image: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
onClick: {
|
||||
type: Function,
|
||||
default: f => f
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
background-size: cover;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
opacity: 0;
|
||||
transition: opacity 300ms ease;
|
||||
will-change: opacity;
|
||||
|
||||
&[lazy="loaded"] {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div :class="{ 'is-shown': show, 'is-contained': contained }" class="cover">
|
||||
<ul class="loader">
|
||||
<li/>
|
||||
<li/>
|
||||
<li/>
|
||||
<li/>
|
||||
<li/>
|
||||
<li/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
contained: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100000;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: all 350ms ease;
|
||||
|
||||
&.is-contained {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.loader li {
|
||||
background: #535353;
|
||||
margin-left: 1px;
|
||||
width: 14px;
|
||||
height: 22px;
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 0px 1px #b4b4b4;
|
||||
transform: skew(-25deg, 0deg) scale(0.1);
|
||||
animation: loader 0.5s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes loader {
|
||||
to {
|
||||
transform: skew(-25deg, 0deg) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loader li:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.loader li:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.loader li:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
.loader li:nth-child(5) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.loader li:nth-child(6) {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
&.is-shown {
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
|
||||
.spinner {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="message-container">
|
||||
<div class="message">
|
||||
<div class="icon">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
<h3>{{ message }}</h3>
|
||||
<p>{{ description }}</p>
|
||||
<slot name="extras"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ""
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-container {
|
||||
margin: 10vh auto;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 2rem;
|
||||
border-radius: 1rem;
|
||||
text-align: center;
|
||||
box-shadow: 0px 1px 2px #b4b4b494;
|
||||
|
||||
.icon > * {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
> * {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div>
|
||||
<app-loader :show="isLoading" />
|
||||
<div v-if="isReady || isSubmitting || isSubmitted" class="container app-fade-in">
|
||||
<Table stripe border :columns="columns" :data="events"></Table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
findEvents,
|
||||
ignoreBand,
|
||||
ignoreDanceHall,
|
||||
ignoreCity,
|
||||
ignoreMunicipality,
|
||||
ignoreState
|
||||
} from "~/utils/graph-client";
|
||||
|
||||
import auth from "~/utils/auth";
|
||||
|
||||
// import LoadFailed from "./Messages/LoadFailed";
|
||||
// import NotViewable from "./Messages/NotViewable";
|
||||
// import SubmitFailed from "./Messages/SubmitFailed";
|
||||
// import SubmitSuccessful from "./Messages/SubmitSuccessful";
|
||||
// import Alerts from "./Alerts";
|
||||
// import TopBar from "./TopBar";
|
||||
// import PackageSelector from "./PackageSelector";
|
||||
// import ProductsForm from "./ProductsForm";
|
||||
// import PropertyInfo from "./Info";
|
||||
// import RealtorFrame from "./RealtorFrame";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// Alerts,
|
||||
// LoadFailed,
|
||||
// NotViewable,
|
||||
// TopBar,
|
||||
// PackageSelector,
|
||||
// ProductsForm,
|
||||
// PropertyInfo,
|
||||
// RealtorFrame,
|
||||
// SubmitFailed,
|
||||
// SubmitSuccessful
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: "loading",
|
||||
property: undefined,
|
||||
propertyId: undefined,
|
||||
order: undefined,
|
||||
orderId: undefined,
|
||||
originalProperty: undefined,
|
||||
selectedPackage: undefined,
|
||||
selectedImages: [],
|
||||
submitError: "",
|
||||
user: undefined,
|
||||
columns: [
|
||||
{
|
||||
title: 'Datum',
|
||||
key: 'date'
|
||||
},
|
||||
{
|
||||
title: 'Tid',
|
||||
key: 'time'
|
||||
},
|
||||
{
|
||||
title: 'Band',
|
||||
render: (h, params) => {
|
||||
const inner = Array(h('span', ' ' + params.row.band.name + ' '));
|
||||
if (this.hasUser) {
|
||||
inner.push(h('Icon', {
|
||||
props: {
|
||||
type: 'ios-eye-off'
|
||||
},
|
||||
attrs: {
|
||||
title: 'Dölj'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.ignoreBand({name: params.row.band.name})
|
||||
}
|
||||
}
|
||||
}, 'Dölj'));
|
||||
}
|
||||
|
||||
return h('div', inner);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Dansställe',
|
||||
render: (h, params) => {
|
||||
const inner = Array(h('span', ' ' + params.row.danceHall.name + ' '));
|
||||
if (this.hasUser) {
|
||||
inner.push(h('Icon', {
|
||||
props: {
|
||||
type: 'ios-eye-off'
|
||||
},
|
||||
attrs: {
|
||||
title: 'Dölj'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.ignoreDanceHall({name: params.row.danceHall.name})
|
||||
}
|
||||
}
|
||||
}, 'Dölj'));
|
||||
}
|
||||
|
||||
return h('div', inner);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Stad',
|
||||
render: (h, params) => {
|
||||
const inner = Array(h('span', ' ' + params.row.danceHall.city + ' '));
|
||||
if (this.hasUser) {
|
||||
inner.push(h('Icon', {
|
||||
props: {
|
||||
type: 'ios-eye-off'
|
||||
},
|
||||
attrs: {
|
||||
title: 'Dölj'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.ignoreCity({name: params.row.danceHall.city})
|
||||
}
|
||||
}
|
||||
}, 'Dölj'));
|
||||
}
|
||||
|
||||
return h('div', inner);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Kommun',
|
||||
render: (h, params) => {
|
||||
const inner = Array(h('span', ' ' + params.row.danceHall.municipality + ' '));
|
||||
if (this.hasUser) {
|
||||
inner.push(h('Icon', {
|
||||
props: {
|
||||
type: 'ios-eye-off'
|
||||
},
|
||||
attrs: {
|
||||
title: 'Dölj'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.ignoreMunicipality({name: params.row.danceHall.municipality})
|
||||
}
|
||||
}
|
||||
}, 'Dölj'));
|
||||
}
|
||||
|
||||
return h('div', inner);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Län',
|
||||
render: (h, params) => {
|
||||
const inner = Array(h('span', ' ' + params.row.danceHall.state + ' '));
|
||||
if (this.hasUser) {
|
||||
inner.push(h('Icon', {
|
||||
props: {
|
||||
type: 'ios-eye-off'
|
||||
},
|
||||
attrs: {
|
||||
title: 'Dölj'
|
||||
},
|
||||
on: {
|
||||
click: () => {
|
||||
this.ignoreState({name: params.row.danceHall.state})
|
||||
}
|
||||
}
|
||||
}, 'Dölj'));
|
||||
}
|
||||
|
||||
return h('div', inner);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Övrigt',
|
||||
key: 'extraInfo'
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.status === "loading";
|
||||
},
|
||||
isLoadFailed() {
|
||||
return this.status === "load-failed";
|
||||
},
|
||||
isReady() {
|
||||
return this.status === "ready";
|
||||
},
|
||||
isSubmitting() {
|
||||
return this.status === "submitting";
|
||||
},
|
||||
isSubmitted() {
|
||||
return this.status === "submitted";
|
||||
},
|
||||
isSubmitFailed() {
|
||||
return this.status === "submit-failed";
|
||||
},
|
||||
isNotViewable() {
|
||||
return this.status === "no-packages";
|
||||
},
|
||||
hasUser() {
|
||||
return this.user;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// const { propertyId, orderId, id } = this.$route.query;
|
||||
this.fetchEvents();
|
||||
this.fetchUser();
|
||||
},
|
||||
methods: {
|
||||
fetchEvents () {
|
||||
this.status = "loading";
|
||||
findEvents()
|
||||
.then(this.eventsFetched)
|
||||
.catch(this.eventsFailed);
|
||||
},
|
||||
eventsFetched(response) {
|
||||
if (response.errors) {
|
||||
throw new Error("Fetch failed");
|
||||
}
|
||||
this.events = response.data.events;
|
||||
this.status = "ready";
|
||||
},
|
||||
eventsFailed() {
|
||||
this.status = "load-failed";
|
||||
},
|
||||
fetchUser() {
|
||||
this.user = auth.getUserInfo();
|
||||
},
|
||||
ignoreBand(name) {
|
||||
ignoreBand(name)
|
||||
.then(this.ignoreSuccess)
|
||||
.catch(this.ignoreFailed)
|
||||
},
|
||||
ignoreDanceHall(name) {
|
||||
ignoreDanceHall(name)
|
||||
.then(this.ignoreSuccess)
|
||||
.catch(this.ignoreFailed)
|
||||
},
|
||||
ignoreCity(name) {
|
||||
ignoreCity(name)
|
||||
.then(this.ignoreSuccess)
|
||||
.catch(this.ignoreFailed)
|
||||
},
|
||||
ignoreMunicipality(name) {
|
||||
ignoreMunicipality(name)
|
||||
.then(this.ignoreSuccess)
|
||||
.catch(this.ignoreFailed)
|
||||
},
|
||||
ignoreState(name) {
|
||||
ignoreState(name)
|
||||
.then(this.ignoreSuccess)
|
||||
.catch(this.ignoreFailed)
|
||||
},
|
||||
ignoreSuccess(response) {
|
||||
this.fetchEvents()
|
||||
},
|
||||
ignoreFailed() {
|
||||
|
||||
}
|
||||
// switchPackage(pack) {
|
||||
// this.selectedPackage = pack;
|
||||
// },
|
||||
// switchImage(image) {
|
||||
// this.selectedImages = [image];
|
||||
// },
|
||||
// publishCampaign(products) {
|
||||
// if (this.status !== "ready") {
|
||||
// return;
|
||||
// }
|
||||
// const packageToOrder = {
|
||||
// products,
|
||||
// packagePrice: this.selectedPackage.price,
|
||||
// packageId: this.selectedPackage.id
|
||||
// };
|
||||
//
|
||||
// this.status = "submitting";
|
||||
// orderPackage({
|
||||
// package: packageToOrder,
|
||||
// propertyId: this.propertyId,
|
||||
// orderId: this.orderId
|
||||
// })
|
||||
// .then(this.publishCampaignSuccess, this.publishCampaignFailed)
|
||||
// .catch(this.publishCampaignFailed);
|
||||
// },
|
||||
// publishCampaignSuccess(response) {
|
||||
// if (response.errors) {
|
||||
// this.publishCampaignFailed();
|
||||
// } else {
|
||||
// this.status = "submitted";
|
||||
// }
|
||||
// },
|
||||
// publishCampaignFailed() {
|
||||
// this.status = "submit-failed";
|
||||
// }
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.left {
|
||||
padding: 1.5rem 1rem;
|
||||
|
||||
> * {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-top: 1px solid #eaeaea;
|
||||
}
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
height: 100vh;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media screen and(max-width: 1200px) {
|
||||
.left {
|
||||
width: 40vw;
|
||||
}
|
||||
.right {
|
||||
width: 60vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and(max-width: 1024px) {
|
||||
.left {
|
||||
width: 50vw;
|
||||
}
|
||||
.right {
|
||||
width: 50vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and(min-width: 1200px) {
|
||||
.left {
|
||||
width: 35vw;
|
||||
}
|
||||
.right {
|
||||
width: 65vw;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,65 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dancefinder-app
|
||||
labels:
|
||||
app: dancefinder-app
|
||||
annotations:
|
||||
kubernetes.io/change-cause: "${TIMESTAMP} Deployed commit id: ${COMMIT}"
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 1
|
||||
minReadySeconds: 30
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dancefinder-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dancefinder-app
|
||||
spec:
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
labelSelector:
|
||||
matchExpressions:
|
||||
- key: "app"
|
||||
operator: In
|
||||
values:
|
||||
- dancefinder-app
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: dancefinder-app
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
imagePullPolicy: Always
|
||||
image: registry.gitlab.com/unboundsoftware/dancefinder/dancefinder-app:${COMMIT}
|
||||
ports:
|
||||
- containerPort: 80
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dancefinder-app
|
||||
labels:
|
||||
app: dancefinder-app
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: dancefinder-app
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
@@ -0,0 +1,16 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: dancefinder-app-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
ingress.kubernetes.io/enable-cors: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: "local-dancefinder.unbound.se"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: dancefinder-app
|
||||
servicePort: 80
|
||||
@@ -0,0 +1,18 @@
|
||||
apiVersion: autoscaling/v2beta1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
labels:
|
||||
app: dancefinder-app
|
||||
name: dancefinder-app
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1beta1
|
||||
kind: Deployment
|
||||
name: dancefinder-app
|
||||
minReplicas: 2
|
||||
maxReplicas: 4
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
targetAverageUtilization: 60
|
||||
@@ -0,0 +1,17 @@
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: dancefinder-app-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/enable-cors: "true"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: "dancefinder.unbound.se"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: dancefinder-app
|
||||
servicePort: 80
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<main>
|
||||
<Layout class="layout">
|
||||
<Menu theme="light" mode="horizontal">
|
||||
<MenuItem v-if="!hasUser" name="user">
|
||||
<span @click="() => { doLogin(); }">Login</span>
|
||||
</MenuItem>
|
||||
<Submenu v-if="hasUser" name="user">
|
||||
<template slot="title">
|
||||
<Avatar :src="user.picture" />
|
||||
{{ user.name }}
|
||||
</template>
|
||||
<nuxt-link to="/logout/">
|
||||
<DropdownItem>
|
||||
Log out
|
||||
</DropdownItem>
|
||||
</nuxt-link>
|
||||
</Submenu>
|
||||
</Menu>
|
||||
<Content>
|
||||
<nuxt />
|
||||
</Content>
|
||||
</Layout>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
// We need this line to import all global styling
|
||||
@import "assets/scss/global.scss";
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout {
|
||||
background-color: white;
|
||||
|
||||
.log-out {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import auth from "~/utils/auth";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
user: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasUser() {
|
||||
return this.user;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
"$route.path"() {
|
||||
this.fetchUser();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUser();
|
||||
},
|
||||
methods: {
|
||||
fetchUser() {
|
||||
this.user = auth.getUserInfo();
|
||||
},
|
||||
doLogin() {
|
||||
auth.triggerLogin({ returnUrl: this.$route.fullPath });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
import auth from '~/utils/auth';
|
||||
|
||||
// do not add trailing slashes to the excluded paths.
|
||||
const excludedPaths = ['', '/authorize', '/logout'];
|
||||
|
||||
// This function is not SSR-compatible, and works since our code currently only runs on the client.
|
||||
export default function ({ isHMR, route }) {
|
||||
// If middleware is called from hot module replacement, ignore it
|
||||
if (isHMR) {
|
||||
return;
|
||||
}
|
||||
|
||||
// support both trailing slash and non-trailing slash so it works the same
|
||||
// when hosted from s3 and not.
|
||||
let path = route.path;
|
||||
if (path.endsWith('/')) {
|
||||
path = path.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!excludedPaths.includes(path)) {
|
||||
if (auth.isExpired()) {
|
||||
// if the auth has just expired - try to log in silently.
|
||||
auth.triggerSilentLogin({ returnUrl: route.fullPath });
|
||||
}
|
||||
else if (!auth.isAuthenticated()) {
|
||||
// otherwise, if the user is not authenticated, trigger a full login.
|
||||
auth.triggerLogin({ returnUrl: route.fullPath });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
sendfile off;
|
||||
|
||||
access_log /dev/stdout combined;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
expires 0;
|
||||
}
|
||||
|
||||
location /health {
|
||||
add_header Content-Type text/plain;
|
||||
access_log off;
|
||||
return 200 'Ok';
|
||||
}
|
||||
|
||||
location /graph/ {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_pass http://dancefinder;
|
||||
}
|
||||
|
||||
error_log /dev/stdout info;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
css: [
|
||||
'iview/dist/styles/iview.css',
|
||||
],
|
||||
head: {
|
||||
link: [
|
||||
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/apple-touch-icon.png' },
|
||||
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/favicon-32x32.png' },
|
||||
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/favicon-16x16.png' },
|
||||
{ rel: 'manifest', href: '/site.webmanifest' },
|
||||
{ rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#7f0aff' },
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
||||
],
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, user-scalable=no, minimal-ui' }
|
||||
],
|
||||
},
|
||||
mode: 'spa',
|
||||
plugins: [
|
||||
{ src: '~/plugins/iview.js', ssr: false },
|
||||
{ src: '~/plugins/graph-routing.js', ssr: false },
|
||||
{ src: '~/plugins/app-components.js', ssr: false },
|
||||
{ src: '~/plugins/vue-lazyload.js', ssr: false }
|
||||
],
|
||||
router: {
|
||||
middleware: ['auth']
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "dancefinder-app",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"author": "Joakim Olsson <joakim@unbound.se>",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"apollo-fetch": "^0.7.0",
|
||||
"auth0-js": "^9.9.0",
|
||||
"eslint": "^5.1.0",
|
||||
"eslint-plugin-vue": "^4.5.0",
|
||||
"iview": "^3.0.0",
|
||||
"lodash": "^4.17.10",
|
||||
"node-sass": "^4.9.0",
|
||||
"nuxt": "^2.0.0",
|
||||
"s-ago": "^1.3.0",
|
||||
"sass-loader": "^7.0.3",
|
||||
"vue": "^2.5.22",
|
||||
"vue-lazyload": "^1.2.6"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development node server/index.js",
|
||||
"build": "nuxt build",
|
||||
"generate": "nuxt generate",
|
||||
"lint": "echo NYI",
|
||||
"precommit": "yarn lint",
|
||||
"prepush": "yarn test",
|
||||
"start": "node server/index.js",
|
||||
"start:ci": "NODE_ENV=production CI=true node server/index.js",
|
||||
"test:cypress": "cypress run",
|
||||
"wait": "wait-on http://localhost:3000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cypress": "^3.1.0",
|
||||
"express-http-proxy": "^1.5.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<app-loader v-if="!authFailed" overlay>
|
||||
<h2>Authenticating...</h2>
|
||||
</app-loader>
|
||||
<app-message v-if="authFailed" :description="err.errorDescription" message="Failed to authenticate.">
|
||||
<template slot="extras">
|
||||
<Button type="primary" @click="triggerLogin">Log in</Button>
|
||||
</template>
|
||||
</app-message>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from "~/utils/auth";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
authFailed: false,
|
||||
err: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
auth
|
||||
.handleAuthentication()
|
||||
.then(this.authorized)
|
||||
.catch(this.unauthorized);
|
||||
},
|
||||
methods: {
|
||||
authorized({ returnUrl }) {
|
||||
this.$router.replace(returnUrl);
|
||||
},
|
||||
unauthorized(err) {
|
||||
this.authFailed = true;
|
||||
this.err = err;
|
||||
},
|
||||
triggerLogin() {
|
||||
const errorState =
|
||||
this.err && this.err.state ? JSON.parse(this.err.state) : null;
|
||||
const returnUrl =
|
||||
errorState && errorState.returnUrl ? errorState.returnUrl : "/";
|
||||
auth.triggerLogin({ returnUrl });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<events />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Events from "~/components/pages/events";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Events
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: "Dancefinder - Events"
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<app-message message="You are logged out.">
|
||||
<h1 slot="icon">🎉</h1>
|
||||
</app-message>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import auth from "~/utils/auth";
|
||||
|
||||
export default {
|
||||
layout: "default",
|
||||
mounted() {
|
||||
auth.logout();
|
||||
this.$router.replace('/')
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
import Vue from 'vue';
|
||||
import AppLoader from '~/components/common/app-loader';
|
||||
import AppMessage from '~/components/common/app-message';
|
||||
import AppLazyBackground from '~/components/common/app-lazy-background';
|
||||
|
||||
Vue.component('app-loader', AppLoader);
|
||||
Vue.component('app-message', AppMessage);
|
||||
Vue.component('app-lazy-background', AppLazyBackground);
|
||||
@@ -0,0 +1,15 @@
|
||||
export default ({ app }) => {
|
||||
app.router.beforeEach((to, from, next) => {
|
||||
// keep the graphql api url variable on all navigation,
|
||||
// if it is actually present.
|
||||
let target;
|
||||
|
||||
if (!to.query.graph && from.query.graph) {
|
||||
target = {
|
||||
path: to.path,
|
||||
query: { ...to.query, graph: from.query.graph },
|
||||
};
|
||||
}
|
||||
next(target);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Vue from 'vue';
|
||||
import iView from 'iview';
|
||||
import locale from 'iview/dist/locale/en-US';
|
||||
|
||||
// add exceptions for these since they conflict with the linting if we use them out of the box
|
||||
Vue.component('i-row', iView.Row);
|
||||
Vue.component('i-col', iView.Col);
|
||||
|
||||
Vue.use(iView, { locale });
|
||||
@@ -0,0 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import VueLazyload from 'vue-lazyload';
|
||||
|
||||
Vue.use(VueLazyload, {
|
||||
lazyComponent: true,
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
const express = require('express');
|
||||
const proxy = require('express-http-proxy');
|
||||
|
||||
const { Nuxt, Builder } = require('nuxt');
|
||||
//const { addGraphMiddleware } = require('graph-mock');
|
||||
|
||||
const app = express();
|
||||
const host = process.env.HOST || '127.0.0.1';
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// Import and set Nuxt.js options
|
||||
let config = require('../nuxt.config.js');
|
||||
config.dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const nuxt = new Nuxt(config);
|
||||
|
||||
// Start build process in dev mode
|
||||
if (config.dev) {
|
||||
const builder = new Builder(nuxt);
|
||||
builder.build();
|
||||
}
|
||||
|
||||
// Add graphql mocking middleware
|
||||
//addGraphMiddleware(app);
|
||||
|
||||
app.use('/graph', proxy('localhost:8081', {
|
||||
proxyReqPathResolver: function (req) {
|
||||
return '/graph';
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Give nuxt middleware to express
|
||||
app.use(nuxt.render);
|
||||
|
||||
// Start express server
|
||||
app.listen(port, host);
|
||||
console.log(`Server is listening on http://${host}:${port}`); // eslint-disable-line
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#7f0aff</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -0,0 +1,327 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve"> <image id="image0" width="256" height="256" x="0" y="0"
|
||||
xlink:href="data:image/png;base64,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
|
||||
MTc4LnN2ZwcXwMcAAAAASUVORK5CYII=" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import auth0 from 'auth0-js';
|
||||
import {
|
||||
storeStateAndNonce,
|
||||
clearStateAndNonce,
|
||||
getStateAndNonce,
|
||||
storeAuth,
|
||||
getIdToken,
|
||||
getUserInfo,
|
||||
clear,
|
||||
getExpiresAt,
|
||||
} from './storage';
|
||||
|
||||
export default class AuthenticationClient {
|
||||
/**
|
||||
* Instantiates an authentication client.
|
||||
* You should only need one per app and authentication method.
|
||||
* @param {Object} auth0Config An auth0 configuration object, as per their API.
|
||||
*/
|
||||
constructor(auth0Config) {
|
||||
this.config = auth0Config;
|
||||
this.webAuth = new auth0.WebAuth(auth0Config);
|
||||
}
|
||||
|
||||
logout() {
|
||||
clear();
|
||||
}
|
||||
|
||||
getUserInfo() {
|
||||
return getUserInfo();
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return !!getIdToken();
|
||||
}
|
||||
|
||||
isExpired() {
|
||||
const expiresAt = getExpiresAt();
|
||||
return new Date().getTime() > expiresAt && this.isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a login by redirecting to auth0.
|
||||
*/
|
||||
triggerLogin(state) {
|
||||
// clear the state and nonce when triggering a login - otherwise hash parsing wont work.
|
||||
clearStateAndNonce();
|
||||
this.webAuth.authorize({ state: JSON.stringify(state) });
|
||||
}
|
||||
|
||||
triggerSilentLogin(state) {
|
||||
if (!state) {
|
||||
throw new Error('You must specify a state.');
|
||||
}
|
||||
const nonce = new Date().getTime().toString();
|
||||
// before we trigger the silent login - store the state and nonce in localstorage.
|
||||
storeStateAndNonce(state, nonce);
|
||||
|
||||
let url = this.webAuth.client.buildAuthorizeUrl({
|
||||
...this.config,
|
||||
nonce,
|
||||
state: JSON.stringify(state),
|
||||
});
|
||||
url += '&prompt=none';
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
storeSession(authResult, resolve, reject) {
|
||||
this.webAuth.client.userInfo(authResult.accessToken, (err, user) => {
|
||||
if (err) {
|
||||
// if any error happens when fetching user info - nuke all session info
|
||||
this.logout();
|
||||
return reject('Authentication failed');
|
||||
}
|
||||
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + new Date().getTime());
|
||||
storeAuth({ ...authResult, user, expiresAt });
|
||||
return resolve(JSON.parse(authResult.state));
|
||||
});
|
||||
}
|
||||
|
||||
handleAuthentication() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// retrieve stored state and nonce from localstorage
|
||||
const { state, nonce } = getStateAndNonce();
|
||||
// however, if there is no state and nonce stored - do not provide any param to parseHash.
|
||||
// Otherwise, the non-silent logins will fail due to invalid state.
|
||||
const parseHashParam = state && nonce ? { state, nonce } : undefined;
|
||||
|
||||
this.webAuth.parseHash(parseHashParam, (err, authResult) => {
|
||||
if (authResult && authResult.accessToken && authResult.idToken) {
|
||||
// If we fail to either set the session or store user info - reject and clear the session.
|
||||
try {
|
||||
return this.storeSession(authResult, resolve, reject);
|
||||
} catch (error) {
|
||||
return reject(error || 'Authentication failed');
|
||||
}
|
||||
} else {
|
||||
return reject(err || 'Authentication failed');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
idToken() {
|
||||
return getIdToken();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import AuthClient, { __RewireAPI__ as rewire } from '.'; // eslint-disable-line
|
||||
import { __RewireAPI__ as storageRewire } from './storage'; // eslint-disable-line
|
||||
|
||||
describe('Auth module', () => {
|
||||
const STORAGE = {
|
||||
ACCESS: storageRewire.__get__('STORAGE_ACCESS'),
|
||||
ID: storageRewire.__get__('STORAGE_ID'),
|
||||
EXPIRES: storageRewire.__get__('STORAGE_EXPIRES'),
|
||||
USER: storageRewire.__get__('STORAGE_USER'),
|
||||
STATE: storageRewire.__get__('STORAGE_STATE'),
|
||||
NONCE: storageRewire.__get__('STORAGE_NONCE'),
|
||||
};
|
||||
let authZero;
|
||||
let TEST_DATE;
|
||||
let webAuth;
|
||||
// populates the storage with some mock data to mimic a login
|
||||
const populateStorage = () => {
|
||||
localStorage.setItem(STORAGE.ACCESS, 'foo');
|
||||
localStorage.setItem(STORAGE.ID, 'foo');
|
||||
localStorage.setItem(STORAGE.EXPIRES, 'foo');
|
||||
localStorage.setItem(STORAGE.USER, '{"foo": "bar"}');
|
||||
localStorage.setItem(STORAGE.STATE, 'foo');
|
||||
localStorage.setItem(STORAGE.NONCE, 'foo');
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// freeze time for this test suite - we're going back to the 90s!
|
||||
TEST_DATE = new Date('1999');
|
||||
global.Date = jest.fn(() => TEST_DATE);
|
||||
// mop up current localStorage before each run - it will not clean itself
|
||||
localStorage.clear();
|
||||
authZero = {
|
||||
authorize: jest.fn(),
|
||||
parseHash: jest.fn(),
|
||||
client: {
|
||||
buildAuthorizeUrl: jest.fn(() => 'https://example.com'),
|
||||
userInfo: jest.fn(),
|
||||
},
|
||||
};
|
||||
rewire.__set__('auth0', { WebAuth: () => authZero });
|
||||
webAuth = new AuthClient();
|
||||
});
|
||||
|
||||
it('stores user info on successful login', (done) => {
|
||||
const state = { returnUrl: '/foo' };
|
||||
const authResult = {
|
||||
accessToken: 'foo',
|
||||
idToken: 'bar',
|
||||
expiresIn: 86400,
|
||||
state: JSON.stringify(state),
|
||||
};
|
||||
const userInfo = { foo: 'bar' };
|
||||
|
||||
authZero.parseHash = jest.fn((options, cb) => {
|
||||
cb(null, authResult);
|
||||
});
|
||||
authZero.client.userInfo = jest.fn((token, cb) => {
|
||||
cb(null, userInfo);
|
||||
});
|
||||
|
||||
return webAuth.handleAuthentication()
|
||||
.then((result) => {
|
||||
const expiresAt = JSON.stringify(authResult.expiresIn * 1000 + TEST_DATE.getTime());
|
||||
|
||||
expect(authZero.parseHash).toBeCalled();
|
||||
expect(localStorage.getItem(STORAGE.ACCESS)).toEqual(authResult.accessToken);
|
||||
expect(localStorage.getItem(STORAGE.ID)).toEqual(authResult.idToken);
|
||||
expect(localStorage.getItem(STORAGE.EXPIRES)).toEqual(expiresAt);
|
||||
expect(localStorage.getItem(STORAGE.USER)).toEqual(JSON.stringify(userInfo));
|
||||
// verify that the state we sent in is what we get back
|
||||
expect(result).toEqual(state);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('clears state and nonce from storage on normal login attempt', () => {
|
||||
populateStorage();
|
||||
webAuth.triggerLogin({ returnUrl: '/' });
|
||||
expect(localStorage.getItem(STORAGE.NONCE)).toBeFalsy();
|
||||
expect(localStorage.getItem(STORAGE.STATE)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('sets correct localstorage content on silent login attempt', () => {
|
||||
const state = { returnUrl: '/some-path/', foo: 'bar', ham: 34 };
|
||||
const serializedState = JSON.stringify(state);
|
||||
|
||||
webAuth.triggerSilentLogin(state);
|
||||
expect(localStorage.getItem(STORAGE.NONCE)).toEqual(TEST_DATE.getTime().toString());
|
||||
expect(localStorage.getItem(STORAGE.STATE)).toEqual(serializedState);
|
||||
});
|
||||
|
||||
it('rejects failed logins', (done) => {
|
||||
const authResult = {
|
||||
error: 'failed to authenticate',
|
||||
};
|
||||
|
||||
authZero.parseHash = jest.fn((options, cb) => {
|
||||
cb(null, authResult);
|
||||
});
|
||||
|
||||
return webAuth.handleAuthentication()
|
||||
.catch(() => {
|
||||
expect(authZero.parseHash).toBeCalled();
|
||||
expect(localStorage.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects logins followed by failed user fetches', (done) => {
|
||||
const authResult = {
|
||||
accessToken: 'foo',
|
||||
idToken: 'bar',
|
||||
expiresIn: 86400,
|
||||
};
|
||||
|
||||
authZero.parseHash = jest.fn((options, cb) => {
|
||||
cb(null, authResult);
|
||||
});
|
||||
authZero.client.userInfo = jest.fn((token, cb) => {
|
||||
cb('Credentials invalid');
|
||||
});
|
||||
|
||||
return webAuth.handleAuthentication()
|
||||
.catch(() => {
|
||||
expect(authZero.parseHash).toBeCalled();
|
||||
expect(authZero.client.userInfo).toBeCalled();
|
||||
expect(localStorage.length).toBe(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('clears session on logout', () => {
|
||||
populateStorage();
|
||||
webAuth.logout();
|
||||
expect(localStorage.length).toBe(0);
|
||||
});
|
||||
|
||||
it('indicates authenticated and expired users based on expiry time', () => {
|
||||
populateStorage();
|
||||
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime() - 1000);
|
||||
expect(webAuth.isAuthenticated()).toBe(true);
|
||||
expect(webAuth.isExpired()).toBe(true);
|
||||
|
||||
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime());
|
||||
expect(webAuth.isAuthenticated()).toBe(true);
|
||||
expect(webAuth.isExpired()).toBe(false);
|
||||
|
||||
localStorage.setItem(STORAGE.EXPIRES, global.Date().getTime() + 1000);
|
||||
expect(webAuth.isAuthenticated()).toBe(true);
|
||||
expect(webAuth.isExpired()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
const STORAGE_ACCESS = 'access_token';
|
||||
const STORAGE_ID = 'id_token';
|
||||
const STORAGE_EXPIRES = 'expires_at';
|
||||
const STORAGE_USER = 'user_info';
|
||||
const STORAGE_STATE = 'auth-state';
|
||||
const STORAGE_NONCE = 'auth-nonce';
|
||||
|
||||
/**
|
||||
* Removes all auth specific items from storage.
|
||||
*/
|
||||
export const clear = () => {
|
||||
localStorage.removeItem(STORAGE_ACCESS);
|
||||
localStorage.removeItem(STORAGE_ID);
|
||||
localStorage.removeItem(STORAGE_EXPIRES);
|
||||
localStorage.removeItem(STORAGE_USER);
|
||||
localStorage.removeItem(STORAGE_STATE);
|
||||
localStorage.removeItem(STORAGE_NONCE);
|
||||
};
|
||||
|
||||
export const storeAuth = ({
|
||||
accessToken, idToken, user, expiresAt,
|
||||
}) => {
|
||||
localStorage.setItem(STORAGE_ACCESS, accessToken);
|
||||
localStorage.setItem(STORAGE_ID, idToken);
|
||||
localStorage.setItem(STORAGE_EXPIRES, expiresAt);
|
||||
localStorage.setItem(STORAGE_USER, JSON.stringify(user));
|
||||
};
|
||||
|
||||
export const storeStateAndNonce = (state, nonce) => {
|
||||
localStorage.setItem(STORAGE_STATE, JSON.stringify(state));
|
||||
localStorage.setItem(STORAGE_NONCE, nonce);
|
||||
};
|
||||
|
||||
export const getStateAndNonce = () => ({
|
||||
state: localStorage.getItem(STORAGE_STATE),
|
||||
nonce: localStorage.getItem(STORAGE_NONCE),
|
||||
});
|
||||
|
||||
export const clearStateAndNonce = () => {
|
||||
localStorage.removeItem(STORAGE_STATE);
|
||||
localStorage.removeItem(STORAGE_NONCE);
|
||||
};
|
||||
|
||||
export const getUserInfo = () => JSON.parse(localStorage.getItem(STORAGE_USER));
|
||||
export const getIdToken = () => localStorage.getItem(STORAGE_ID);
|
||||
export const getExpiresAt = () => JSON.parse(localStorage.getItem(STORAGE_EXPIRES));
|
||||
@@ -0,0 +1,18 @@
|
||||
import AuthClient from './auth-client';
|
||||
|
||||
const getRedirectUri = () => {
|
||||
return location ? `${location.origin}/authorize/` : "http://localhost:3000/authorize/";
|
||||
};
|
||||
|
||||
const auth0Config = {
|
||||
domain: "unbound.eu.auth0.com",
|
||||
clientID: "orQfnvCPUR5C3mJkKoiWLQHOVQsBn60e",
|
||||
redirectUri: getRedirectUri(),
|
||||
audience: "https://unbound.eu.auth0.com/userinfo",
|
||||
responseType: "token id_token",
|
||||
scope: "openid profile"
|
||||
};
|
||||
|
||||
const webAuth = new AuthClient(auth0Config);
|
||||
|
||||
export default webAuth;
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
findEvents,
|
||||
} from '../index';
|
||||
|
||||
const verifyResponse = (response) => {
|
||||
expect(response.errors).toBe(undefined);
|
||||
};
|
||||
|
||||
const verifyError = (response) => {
|
||||
expect(response.errors.length).toBeGreaterThan(0);
|
||||
};
|
||||
|
||||
describe('GQL Queries', () => {
|
||||
test('findEvents', () => findEvents().then(verifyResponse));
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
findEvents
|
||||
} from './queries';
|
||||
|
||||
export {
|
||||
ignoreBand,
|
||||
ignoreDanceHall,
|
||||
ignoreCity,
|
||||
ignoreMunicipality,
|
||||
ignoreState
|
||||
} from './mutations';
|
||||
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
includeCredentials: (tokenFn) => {
|
||||
return ({options}, next) => {
|
||||
if (!options.headers) {
|
||||
options.headers = {}; // Create the headers object if needed.
|
||||
}
|
||||
const token = tokenFn();
|
||||
if (token) {
|
||||
options.headers['Authorization'] = 'Bearer ' + tokenFn();
|
||||
}
|
||||
options.credentials = 'same-origin'; // eslint-disable-line
|
||||
next();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
ignoreBandMutation: `
|
||||
mutation IgnoreBand($name: String!) {
|
||||
ignore: IgnoreBand(name: $name)
|
||||
}
|
||||
`,
|
||||
ignoreDanceHallMutation: `
|
||||
mutation IgnoreDanceHall($name: String!) {
|
||||
ignore: IgnoreDanceHall(name: $name)
|
||||
}
|
||||
`,
|
||||
ignoreCityMutation: `
|
||||
mutation IgnoreCity($name: String!) {
|
||||
ignore: IgnoreCity(name: $name)
|
||||
}
|
||||
`,
|
||||
ignoreMunicipalityMutation: `
|
||||
mutation IgnoreMunicipality($name: String!) {
|
||||
ignore: IgnoreMunicipality(name: $name)
|
||||
}
|
||||
`,
|
||||
ignoreStateMutation: `
|
||||
mutation IgnoreState($name: String!) {
|
||||
ignore: IgnoreState(name: $name)
|
||||
}
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createQuery } from './utils';
|
||||
import {
|
||||
ignoreBandMutation,
|
||||
ignoreDanceHallMutation,
|
||||
ignoreCityMutation,
|
||||
ignoreMunicipalityMutation,
|
||||
ignoreStateMutation,
|
||||
} from './mutationStrings';
|
||||
import webAuth from '../auth';
|
||||
|
||||
/* eslint-disable max-len */
|
||||
export const ignoreBand = variables => {
|
||||
return createQuery(webAuth.idToken, ignoreBandMutation, variables)
|
||||
};
|
||||
export const ignoreDanceHall = variables => {
|
||||
return createQuery(webAuth.idToken, ignoreDanceHallMutation, variables)
|
||||
};
|
||||
export const ignoreCity = variables => {
|
||||
return createQuery(webAuth.idToken, ignoreCityMutation, variables)
|
||||
};
|
||||
export const ignoreMunicipality = variables => {
|
||||
return createQuery(webAuth.idToken, ignoreMunicipalityMutation, variables)
|
||||
};
|
||||
export const ignoreState = variables => {
|
||||
return createQuery(webAuth.idToken, ignoreStateMutation, variables)
|
||||
};
|
||||
/* eslint-enable max-len */
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createQuery } from './utils';
|
||||
import {
|
||||
eventQuery,
|
||||
} from './queryStrings';
|
||||
import webAuth from '../auth';
|
||||
|
||||
|
||||
/* eslint-disable max-len */
|
||||
export const findEvents = () => createQuery(webAuth.idToken, eventQuery);
|
||||
/* eslint-enable max-len */
|
||||
@@ -0,0 +1,18 @@
|
||||
export const eventQuery = `
|
||||
{
|
||||
events: Events {
|
||||
date
|
||||
time
|
||||
band {
|
||||
name
|
||||
}
|
||||
danceHall {
|
||||
name
|
||||
city
|
||||
municipality
|
||||
state
|
||||
}
|
||||
extraInfo
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,13 @@
|
||||
const { createApolloFetch } = require('apollo-fetch');
|
||||
const { includeCredentials } = require('./middleware');
|
||||
|
||||
const defaultGraphUri = '/graph';
|
||||
|
||||
export const createQuery = (tokenFn, query, variables) => { // eslint-disable-line
|
||||
const apollo = createApolloFetch({ uri: defaultGraphUri });
|
||||
|
||||
apollo.use(includeCredentials(tokenFn));
|
||||
// apollo.useAfter(trackErrors);
|
||||
|
||||
return apollo({ query, variables });
|
||||
};
|
||||