Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite authentication logic with the API store #373

Merged
merged 29 commits into from
Feb 23, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2eb6e09
Use constants for localStorage
carlobeltrame Feb 17, 2020
c3a7dca
Don't reload entities that are still in loading state.
carlobeltrame Feb 17, 2020
ea03931
Return the resulting entity from a POST or PATCH call, and the delete…
carlobeltrame Feb 17, 2020
1cfc04d
Prefix the API root path to self links retrieved from the store
carlobeltrame Feb 17, 2020
6b92eb0
Add href function to API store and expose post and reload for use in …
carlobeltrame Feb 17, 2020
1dbc6db
Clean up RPC endpoints in backend, so the API store can work with it
carlobeltrame Feb 17, 2020
421684a
Rewrite the authentication logic using the Vuex API store
carlobeltrame Feb 17, 2020
cf6c073
Unify callback behaviour in authentication code
carlobeltrame Feb 17, 2020
d30037e
Simplify authentication code and fix a bug with reloading from the AP…
carlobeltrame Feb 18, 2020
6490b3e
Only add the ugly redirect query param when necessary
carlobeltrame Feb 18, 2020
f350b70
Use await instead of callbacks
carlobeltrame Feb 18, 2020
e7116b5
Rename the login relation to auth and the native login endpoint to login
carlobeltrame Feb 18, 2020
da5b618
Stop using localStorage for login state, and fix some advanced loaded…
carlobeltrame Feb 19, 2020
a3fc02e
Replace ugly login status refresh logic in layouts with reactive impl…
carlobeltrame Feb 19, 2020
10e0ad2
Use only this. or directly import the auth functions. Fixes #318
carlobeltrame Feb 19, 2020
724767f
Add loading spinners for login as well
carlobeltrame Feb 19, 2020
07d425f
Rename the loaded promise to load (see #359)
carlobeltrame Feb 19, 2020
1121929
Codestyle: Don't return .items in computed. Fixes #364 for now
carlobeltrame Feb 19, 2020
aacc45d
Rewrite the registering process using the API store
carlobeltrame Feb 19, 2020
a2d9f50
Fix specs that were broken by returning the absolute URIs from storeV…
carlobeltrame Feb 19, 2020
f38f55a
Add tests for fixed load promise bugs
carlobeltrame Feb 19, 2020
5019266
Improve behaviour and documentation of post requests
carlobeltrame Feb 20, 2020
4f9f3ba
Fix the href function
carlobeltrame Feb 20, 2020
d3e92fb
Fix the load promise during a PATCH call
carlobeltrame Feb 20, 2020
889d49c
Add tests for the squashed bugs and new href function
carlobeltrame Feb 20, 2020
9fdb85d
Simplify unnecessary imports in tests
carlobeltrame Feb 20, 2020
e83849d
Add unit tests for the authentication logic
carlobeltrame Feb 20, 2020
0c7dbea
Harden authentication logic tests
carlobeltrame Feb 21, 2020
141b660
Harden authentication logic tests
carlobeltrame Feb 21, 2020
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
2 changes: 1 addition & 1 deletion backend/module/eCampApi/config/documentation.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'eCampApi\\V1\\Rpc\\Index\\Controller' => array(
'description' => 'Entrypoint',
),
'eCampApi\\V1\\Rpc\\Login\\Controller' => array(
'eCampApi\\V1\\Rpc\\Auth\\Controller' => array(
'description' => '',
'GET' => array(
'description' => '/index
Expand Down
30 changes: 15 additions & 15 deletions backend/module/eCampApi/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,19 @@
'e-camp-api.rpc.index' => array(
'type' => 'Segment',
'options' => array(
'route' => '/api[/]',
'route' => '/api',
'defaults' => array(
'controller' => 'eCampApi\\V1\\Rpc\\Index\\Controller',
'action' => 'index',
),
),
),
'e-camp-api.rpc.login' => array(
'e-camp-api.rpc.auth' => array(
'type' => 'Segment',
'options' => array(
'route' => '/api/login[/:action]',
'route' => '/api/auth[/:action]',
'defaults' => array(
'controller' => 'eCampApi\\V1\\Rpc\\Login\\Controller',
'controller' => 'eCampApi\\V1\\Rpc\\Auth\\Controller',
'action' => 'index',
),
),
Expand Down Expand Up @@ -162,7 +162,7 @@
),
'controllers' => array(
'factories' => array(
'eCampApi\\V1\\Rpc\\Login\\Controller' => 'eCampApi\\V1\\Rpc\\Login\\LoginControllerFactory',
'eCampApi\\V1\\Rpc\\Auth\\Controller' => 'eCampApi\\V1\\Rpc\\Auth\\AuthControllerFactory',
'eCampApi\\V1\\Rpc\\Index\\Controller' => 'eCampApi\\V1\\Rpc\\Index\\IndexControllerFactory',
'eCampApi\\V1\\Rpc\\Register\\Controller' => 'eCampApi\\V1\\Rpc\\Register\\RegisterControllerFactory',
),
Expand All @@ -181,7 +181,7 @@
9 => 'e-camp-api.rest.doctrine.user',
10 => 'e-camp-api.rest.doctrine.camp-collaboration',
12 => 'e-camp-api.rpc.index',
11 => 'e-camp-api.rpc.login',
11 => 'e-camp-api.rpc.auth',
13 => 'e-camp-api.rpc.register',
14 => 'e-camp-api.rest.doctrine.event-plugin',
15 => 'e-camp-api.rest.doctrine.plugin',
Expand Down Expand Up @@ -554,7 +554,7 @@
'eCampApi\\V1\\Rest\\User\\Controller' => 'HalJson',
'eCampApi\\V1\\Rest\\CampCollaboration\\Controller' => 'HalJson',
'eCampApi\\V1\\Rpc\\Index\\Controller' => 'HalJson',
'eCampApi\\V1\\Rpc\\Login\\Controller' => 'HalJson',
'eCampApi\\V1\\Rpc\\Auth\\Controller' => 'HalJson',
'eCampApi\\V1\\Rpc\\Register\\Controller' => 'Json',
'eCampApi\\V1\\Rest\\EventPlugin\\Controller' => 'HalJson',
'eCampApi\\V1\\Rest\\Plugin\\Controller' => 'HalJson',
Expand Down Expand Up @@ -621,7 +621,7 @@
1 => 'application/json',
2 => 'application/*+json',
),
'eCampApi\\V1\\Rpc\\Login\\Controller' => array(
'eCampApi\\V1\\Rpc\\Auth\\Controller' => array(
0 => 'application/vnd.e-camp-api.v1+json',
1 => 'application/json',
2 => 'application/*+json',
Expand Down Expand Up @@ -696,7 +696,7 @@
0 => 'application/vnd.e-camp-api.v1+json',
1 => 'application/json',
),
'eCampApi\\V1\\Rpc\\Login\\Controller' => array(
'eCampApi\\V1\\Rpc\\Auth\\Controller' => array(
0 => 'application/vnd.e-camp-api.v1+json',
1 => 'application/json',
),
Expand Down Expand Up @@ -948,8 +948,8 @@
'eCampApi\\V1\\Rest\\CampCollaboration\\Controller' => array(
'input_filter' => 'eCampApi\\V1\\Rest\\CampCollaboration\\Validator',
),
'eCampApi\\V1\\Rpc\\Login\\Controller' => array(
'input_filter' => 'eCampApi\\V1\\Rpc\\Login\\Validator',
'eCampApi\\V1\\Rpc\\Auth\\Controller' => array(
'input_filter' => 'eCampApi\\V1\\Rpc\\Auth\\Validator',
),
'eCampApi\\V1\\Rest\\EventPlugin\\Controller' => array(
'input_filter' => 'eCampApi\\V1\\Rest\\EventPlugin\\Validator',
Expand Down Expand Up @@ -1446,7 +1446,7 @@
'validators' => array(),
),
),
'eCampApi\\V1\\Rpc\\Login\\Validator' => array(),
'eCampApi\\V1\\Rpc\\Auth\\Validator' => array(),
'eCampApi\\V1\\Rest\\EventPlugin\\Validator' => array(
0 => array(
'name' => 'instanceName',
Expand Down Expand Up @@ -1599,13 +1599,13 @@
),
),
'zf-rpc' => array(
'eCampApi\\V1\\Rpc\\Login\\Controller' => array(
'service_name' => 'Login',
'eCampApi\\V1\\Rpc\\Auth\\Controller' => array(
'service_name' => 'Auth',
'http_methods' => array(
0 => 'GET',
1 => 'POST',
),
'route_name' => 'e-camp-api.rpc.login',
'route_name' => 'e-camp-api.rpc.auth',
),
'eCampApi\\V1\\Rpc\\Index\\Controller' => array(
'service_name' => 'Index',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?php
namespace eCampApi\V1\Rpc\Login;
namespace eCampApi\V1\Rpc\Auth;

use eCamp\Core\Auth\Adapter\LoginPassword;
use eCamp\Core\Entity\User;
Expand All @@ -13,7 +13,7 @@
use ZF\Hal\Link\Link;
use ZF\Hal\View\HalJsonModel;

class LoginController extends AbstractActionController {
class AuthController extends AbstractActionController {
/** @var AuthenticationService */
private $authenticationService;

Expand Down Expand Up @@ -68,27 +68,48 @@ public function indexAction() {
'route' => [ 'name' => 'e-camp-api.rpc.index' ]
]);

$data['login'] = Link::factory([
'rel' => 'login',
'route' => [
'name' => 'e-camp-api.rpc.auth',
'params' => [ 'action' => 'login' ]
]
]);

$data['register'] = Link::factory([
'rel' => 'register',
'route' => [
'name' => 'e-camp-api.rpc.register',
'params' => [ 'action' => 'register' ]
]
]);

$data['google'] = Link::factory([
'rel' => 'google',
'route' => [
'name' => 'e-camp-api.rpc.login',
'params' => [ 'action' => 'google' ]
]
'rel' => 'google',
'route' => [
'name' => 'e-camp-api.rpc.auth',
'params' => [ 'action' => 'google' ]
]
]);

$data['pbsmidata'] = Link::factory([
'rel' => 'pbsmidata',
'route' => [
'name' => 'e-camp-api.rpc.login',
'name' => 'e-camp-api.rpc.auth',
'params' => [ 'action' => 'pbsmidata' ]
]
]);

$data['self'] = Link::factory([
'rel' => 'self',
'route' => 'e-camp-api.rpc.auth'
]);

if ($userId != null) {
$data['logout'] = Link::factory([
'rel' => 'logout',
'route' => [
'name' => 'e-camp-api.rpc.login',
'name' => 'e-camp-api.rpc.auth',
'params' => [ 'action' => 'logout' ]
]
]);
Expand All @@ -115,7 +136,7 @@ public function loginAction() {
$adapter = new LoginPassword($user, $password);
$this->authenticationService->authenticate($adapter);

return $this->redirect()->toRoute('e-camp-api.rpc.login');
return $this->redirect()->toRoute('e-camp-api.rpc.auth');
}


Expand All @@ -127,7 +148,7 @@ public function googleAction() {
$request = $this->getRequest();
$externalCallback = $request->getQuery('callback');

$redirect = $this->url()->fromRoute('e-camp-api.rpc.login', [], ['query'=>['callback'=>$externalCallback]]);
$redirect = $this->url()->fromRoute('e-camp-api.rpc.auth', [], ['query'=>['callback'=>$externalCallback]]);

return $this->redirect()->toRoute(
'ecamp.auth/google',
Expand All @@ -144,7 +165,7 @@ public function pbsMiDataAction() {
$request = $this->getRequest();
$externalCallback = $request->getQuery('callback');

$redirect = $this->url()->fromRoute('e-camp-api.rpc.login', [], [
$redirect = $this->url()->fromRoute('e-camp-api.rpc.auth', [], [
'query'=>['callback'=>$externalCallback]
]);

Expand All @@ -162,6 +183,6 @@ public function pbsMiDataAction() {
public function logoutAction() {
$this->authenticationService->clearIdentity();

return $this->redirect()->toRoute('e-camp-api.rpc.login');
return $this->redirect()->toRoute('e-camp-api.rpc.auth');
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<?php
namespace eCampApi\V1\Rpc\Login;
namespace eCampApi\V1\Rpc\Auth;

use eCamp\Core\EntityService\UserService;
use Zend\Authentication\AuthenticationService;

class LoginControllerFactory {
class AuthControllerFactory {
public function __invoke($controllers) {
$authenticationService = $controllers->get(AuthenticationService::class);

/** @var UserService $userService */
$userService = $controllers->get(UserService::class);

return new LoginController($authenticationService, $userService);
return new AuthController($authenticationService, $userService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ public function indexAction() {
$data['user'] = 'guest';
}

$data['login'] = Link::factory([
'rel' => 'login',
'route' => 'e-camp-api.rpc.login'
$data['auth'] = Link::factory([
'rel' => 'auth',
'route' => 'e-camp-api.rpc.auth'
]);

$data['self'] = Link::factory([
Expand Down
2 changes: 1 addition & 1 deletion backend/module/eCampLib/config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
\Interop\Container\ContainerInterface::class => Zend\ServiceManager\ServiceManager::class,
],
],
],
],
],
],

Expand Down
106 changes: 49 additions & 57 deletions frontend/src/auth.js
Original file line number Diff line number Diff line change
@@ -1,70 +1,62 @@
import Vue from 'vue'
import axios from 'axios'
const storageLocation = 'loggedIn'
import { get, reload, post, href } from '@/store'
import router from '@/router'

const subscribers = []
axios.interceptors.response.use(null, error => {
if (error.status === 401) {
logout()
}
return Promise.reject(error)
})

const notifySubscribers = newLoginStatus => {
subscribers.forEach(subscriber => subscriber(newLoginStatus))
function isLoggedIn () {
return get().auth().role === 'user'
}

export const auth = {
async isLoggedIn () {
const savedStatus = window.localStorage.getItem(storageLocation)
if (savedStatus !== null) {
return savedStatus === '1'
}
const response = await axios.get(process.env.VUE_APP_ROOT_API + '/login')
let loggedIn = '0'
if (response.data.user !== 'guest') {
loggedIn = '1'
}
window.localStorage.setItem(storageLocation, loggedIn)
return loggedIn === '1'
},
subscribe (onLoginStatusChange) {
subscribers.push(onLoginStatusChange)
},
login (username, password) {
return axios.post(process.env.VUE_APP_ROOT_API + '/login/login', { username: username, password: password })
.then(resp => {
if (resp.data.user !== 'guest') {
this.loginSuccess()
return true
} else {
return false
}
})
},
loginGoogle (returnUrl) {
window.open(process.env.VUE_APP_ROOT_API + '/login/google?callback=' + encodeURI(returnUrl), '', 'width=500px,height=600px')
},
loginPbsMiData (returnUrl) {
window.open(process.env.VUE_APP_ROOT_API + '/login/pbsmidata?callback=' + encodeURI(returnUrl), '', 'width=500px,height=600px')
},
loginSuccess () {
window.localStorage.setItem(storageLocation, '1')
notifySubscribers(true)
},
logout (callback) {
axios.get(process.env.VUE_APP_ROOT_API + '/login/logout').then(response => {
window.localStorage.setItem(storageLocation, '0')
notifySubscribers(false)
if (callback) {
callback(response)
}
export async function refreshLoginStatus (forceReload = true) {
if (forceReload) reload(get().auth())
await get().auth()._meta.load
return isLoggedIn()
}

async function login (username, password) {
const url = await href(get().auth(), 'login')
return post(url, { username: username, password: password }).then(() => refreshLoginStatus())
}

async function register ({ username, email, password }) {
const url = await href(get().auth(), 'register')
return post(url, { username, email, password }).then(() => refreshLoginStatus())
}

async function oAuthLoginInSeparateWindow (provider) {
return new Promise(resolve => {
// Make the promise resolve function available on global level, so the separate window can call it
window.afterLogin = resolve

href(get().auth(), provider).then(url => {
const returnUrl = window.location.origin + router.resolve({ name: 'loginCallback' }).href
// TODO use templated relations once #369 is implemented
window.open(url + '?callback=' + encodeURI(returnUrl), '', 'width=500px,height=600px')
})
}
})
}

axios.interceptors.response.use(null, error => {
if (error.status === 401) {
auth.logout()
}
return Promise.reject(error)
})
async function loginGoogle () {
return oAuthLoginInSeparateWindow('google').then(() => refreshLoginStatus())
}

async function loginPbsMiData () {
return oAuthLoginInSeparateWindow('pbsmidata').then(() => refreshLoginStatus())
}

async function logout () {
return reload(get().auth().logout())._meta.load.then(() => refreshLoginStatus())
}

export const auth = { isLoggedIn, refreshLoginStatus, login, register, loginGoogle, loginPbsMiData, logout }

Vue.auth = auth
Object.defineProperties(Vue.prototype, {
$auth: {
get () {
Expand Down
Loading