Skip to content

Commit

Permalink
Rely on authentication token stored in cookie (#817)
Browse files Browse the repository at this point in the history
* chore: consume platform api on rewritten same domain path

* feat: remove local storage usage

* fix: make sure login state is known on first render

* refactor: handle login state while app is being displayed

* fix: initializing auth store should only ever happen once

* chore: enable api proxying in netlify preview

* chore: configure webpack dev server to proxy api

* chore: normalize filenames

* fix: proxy rule is overwritten
  • Loading branch information
m90 authored May 6, 2024
1 parent 4b353d9 commit 00120ef
Show file tree
Hide file tree
Showing 14 changed files with 126 additions and 85 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/netlify-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ jobs:
- run: npm ci
- run: npm run build

- name: Enable SPA routing on Netlify
- name: Enable SPA routing and API proxying on Netlify
run: |
echo "/* /index.html 200" > ./dist/_redirects
echo "/api/* https://api.wikibase.cloud/:splat 200" > ./dist/_redirects
echo "/* /index.html 200" >> ./dist/_redirects
- name: Deploy branch preview to Netlify
id: netlify_deploy
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ RUN VUE_APP_BUILD_FOR_DOCKER_IMAGE=1 npm run build
FROM nginx:1-alpine

LABEL org.opencontainers.image.source="https://github.com/wbstack/ui"
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=builder --chown=nginx:nginx /src/app/dist /usr/share/nginx/html
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY src/config.template.js /config.template.js
COPY ./nginx/default.template.conf /default.template.conf

ENTRYPOINT ["/docker-entrypoint.sh"]

Expand Down
1 change: 1 addition & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ set -eu

export DOLLAR='$'
envsubst < /config.template.js > /usr/share/nginx/html/config.js
envsubst '$API_URL' < /default.template.conf > /etc/nginx/conf.d/default.conf

exec "$@"
4 changes: 4 additions & 0 deletions nginx/default.conf → nginx/default.template.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ server {
root /usr/share/nginx/html;
index index.html index.html;

location /api/(.*) {
proxy_pass ${API_URL}/$1;
}

# Always serve index.html for any request
location / {
root /usr/share/nginx/html;
Expand Down
28 changes: 26 additions & 2 deletions src/backend/api.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import axios from './axios'

/* User endpoints */
export const login = async user => (await axios.post('/auth/login', user)).data
export const login = async (user) => {
let call
if (user) {
call = axios.post('/auth/login', user)
} else {
call = axios.get('/auth/login')
}
try {
const { data } = await call
return data
} catch (err) {
if (err.response.code === '401') {
return null
}
throw err
}
}

export const logout = async (user) => {
return await axios.delete('/auth/login', user)
}

export const register = async payload => {
const resp = await axios.post('/user/register', payload).catch(ex => {
const { errors = {} } = ex.response.data
Expand All @@ -27,7 +48,10 @@ export const verifyEmail = async payload => {
throw expired
})
}
export const checkVerified = async () => (await axios.post('/user/self')).data.data.verified
export const checkVerified = async () => {
const { data } = await axios.get('/auth/login')
return data.verified
}

/* Wiki endpoints */
export const countWikis = async () => (await axios.get('/wiki/count')).data.data // TODO This doesn't seem to exist and not used?
Expand Down
25 changes: 1 addition & 24 deletions src/backend/axios.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
/* global localStorage */

import Axios from 'axios'
import config from '~/config'

const axios = Axios.create({
baseURL: config.API_URL,
baseURL: '/api',
headers: {
'Content-Type': 'application/json'
}
})

/*
* The interceptor here ensures that we check for the token in local storage
* every time an ajax request is made
*/
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem('auth')

if (token) {
config.headers.Authorization = `Bearer ${token}`
}

return config
},

(error) => {
return Promise.reject(error)
}
)

export default axios
63 changes: 39 additions & 24 deletions src/backend/mocks/default_handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { rest } from 'msw'

let myWikis = JSON.parse(localStorage.getItem('msw-myWikis')) || []
let lastWikiId = (myWikis.length && myWikis[myWikis.length - 1].id) || 0
let user = JSON.parse(localStorage.getItem('user'))
let user = makeUser()

const makeUser = (email = 'test@local') => ({
id: 1,
email: email,
verified: true,
created_at: '2020-01-01',
updated_at: '2020-01-01'
})
function makeUser (email = 'test@local') {
return {
id: 1,
email: email,
verified: true,
created_at: '2020-01-01',
updated_at: '2020-01-01'
}
}

const makeNewWiki = ({ domain, sitename }) => {
const newWiki = {
Expand Down Expand Up @@ -129,24 +131,37 @@ const wikiDiscovery = (referrer, params) => {

export const handlers = [
/* User endpoints */
rest.post(/\/auth\/login$/, (req, res, ctx) => {
rest.post(/\/api\/auth\/login$/, (req, res, ctx) => {
user = makeUser(req.body.email)
return res(ctx.json({
user
}), ctx.cookie('authToken', 'token_value'))
}),
rest.get(/\/api\/auth\/login$/, (req, res, ctx) => {
const { authToken } = req.cookies
if (authToken !== 'token_value') {
return res(ctx.status(401))
}
user = makeUser(req.body.email)
return res(ctx.json({
user,
token: 'test_token'
user
}))
}),
rest.post(/\/user\/forgotPassword$/, (req, res, ctx) => {
rest.delete(/\/api\/auth\/login$/, (req, res, ctx) => {
user = makeUser(req.body.email)
return res(ctx.status(204))
}),
rest.post(/\/api\/user\/forgotPassword$/, (req, res, ctx) => {
if (req.body.email === 'serverError@example.com') {
return res(ctx.status(400, 'Mocked Server Error'))
}
return res(ctx.status(200))
}
),
rest.post(/\/user\/resetPassword$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/user\/sendVerifyEmail$/, (_, res, ctx) => res(ctx.json({ message: 'Already verified' }))),
rest.post(/\/user\/verifyEmail$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/contact\/sendMessage$/, (req, res, ctx) => {
rest.post(/\/api\/user\/resetPassword$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/api\/user\/sendVerifyEmail$/, (_, res, ctx) => res(ctx.json({ message: 'Already verified' }))),
rest.post(/\/api\/user\/verifyEmail$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/api\/contact\/sendMessage$/, (req, res, ctx) => {
if (req.body.name === 'recaptchaError') {
return res(ctx.status(401, 'Mocked recaptcha Error'))
}
Expand All @@ -161,12 +176,12 @@ export const handlers = [
),

/* Wiki endpoints */
rest.get(/\/wiki\/count$/, (_, res, ctx) => res(ctx.json({ data: 1 }))),
rest.post(/\/wiki\/mine$/, (_, res, ctx) => res(ctx.json({ wikis: myWikis, count: myWikis.length, limit: false }))),
rest.post(/\/wiki\/create$/, (req, res, ctx) => {
rest.get(/\/api\/wiki\/count$/, (_, res, ctx) => res(ctx.json({ data: 1 }))),
rest.post(/\/api\/wiki\/mine$/, (_, res, ctx) => res(ctx.json({ wikis: myWikis, count: myWikis.length, limit: false }))),
rest.post(/\/api\/wiki\/create$/, (req, res, ctx) => {
return res(ctx.json({ data: makeNewWiki(req.body) }))
}),
rest.post(/\/wiki\/delete$/, (req, res, ctx) => {
rest.post(/\/api\/wiki\/delete$/, (req, res, ctx) => {
const wikiId = req.body.wiki
const wikiIndex = myWikis.findIndex(w => w.id === Number(wikiId))
if (wikiIndex < 0) {
Expand All @@ -176,17 +191,17 @@ export const handlers = [
removeWiki(wikiIndex)
return res(ctx.status(200))
}),
rest.post(/\/wiki\/logo\/update$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/wiki\/setting\/.*?\/update$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/wiki\/details$/, (req, res, ctx) => {
rest.post(/\/api\/wiki\/logo\/update$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/api\/wiki\/setting\/.*?\/update$/, (_, res, ctx) => res(ctx.status(200))),
rest.post(/\/api\/wiki\/details$/, (req, res, ctx) => {
const wikiId = req.body.wiki
const wikiDetails = myWikis.find(w => w.id === Number(wikiId))
if (!wikiDetails) {
return res(ctx.status(404))
}
return res(ctx.json({ data: wikiDetails }), ctx.status(200))
}),
rest.get(/\/wiki$/, (req, res, ctx) => {
rest.get(/\/api\/wiki$/, (req, res, ctx) => {
return res(ctx.json(wikiDiscovery(req.referrer, req.url.searchParams)))
})
]
5 changes: 5 additions & 0 deletions src/components/Layout/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<img class="logo" src="../../assets/logo.svg">
</a>
<v-spacer></v-spacer>
<template v-if="!isInitializing">
<template v-if="isLoggedIn">
<v-btn class="text-none no-button-pointer-events" text> Hi {{ currentUser.email }}</v-btn>
<v-btn id="nav-dashboard" text to="/dashboard">Dashboard</v-btn>
Expand Down Expand Up @@ -48,6 +49,7 @@
Sign Up
</v-btn>
</template>
</template>
</v-toolbar>
</template>

Expand All @@ -58,6 +60,9 @@ export default {
isLoggedIn: function () {
return this.$store.getters.isLoggedIn
},
isInitializing: function () {
return this.$store.getters.authStatus === 'initializing'
},
currentUser: function () {
return this.$store.getters.currentUser
}
Expand Down
1 change: 0 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// keep in sync with src/config.template.js
export default {
RECAPTCHA_SITE_KEY: process.env.VUE_APP_RECAPTCHA_SITE_KEY,
API_URL: process.env.VUE_APP_API_URL,
SUBDOMAIN_SUFFIX: process.env.VUE_APP_SUBDOMAIN_SUFFIX,
CNAME_RECORD: process.env.VUE_APP_CNAME_RECORD,
API_MOCK: process.env.VUE_APP_API_MOCK
Expand Down
1 change: 0 additions & 1 deletion src/config.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// keep in sync with src/config.js
window.config = {
RECAPTCHA_SITE_KEY: '${RECAPTCHA_SITE_KEY}',
API_URL: '${API_URL}',
SUBDOMAIN_SUFFIX: '${SUBDOMAIN_SUFFIX}',
CNAME_RECORD: '${CNAME_RECORD}',
API_MOCK: '${API_MOCK}'
Expand Down
1 change: 1 addition & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ new Vue({
components: { App },
template: '<App/>',
created: function () {
store.dispatch('login', null)
axios.interceptors.response.use(undefined, function (err) {
return new Promise(function (resolve, reject) {
// Unauthenticated. is the exact error message returned by the API for the auth middle ware
Expand Down
3 changes: 2 additions & 1 deletion src/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ const router = new Router({

// Require some routes to be logged in only.
// From https://pusher.com/tutorials/authentication-vue-vuex
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
await Store.getters.initialized
if (Store.getters.isLoggedIn) {
next()
return
Expand Down
Loading

0 comments on commit 00120ef

Please sign in to comment.