Skip to content
Snippets Groups Projects
auth.js 10.7 KiB
Newer Older
import m from 'mithril';
import axios from 'axios';
import ClientOAuth2 from 'client-oauth2';
import { Snackbar } from 'polythene-mithril';
Hermann's avatar
Hermann committed
// eslint-disable-next-line import/extensions
import { apiUrl, ownUrl, oAuthID } from 'networkConfig';
import * as localStorage from './localStorage';
import config from './resourceConfig.json';
// Object which stores the current login-state
const APISession = {
  authenticated: false,
  token: '',
Hermann's avatar
Hermann committed
  userID: null,
Sandro Lutz's avatar
Sandro Lutz committed
const amivapi = axios.create({
  baseURL: apiUrl,
  headers: { 'Content-Type': 'application/json' },
  validateStatus: () => true,
// OAuth Handler
const oauth = new ClientOAuth2({
  clientId: oAuthID,
  authorizationUri: `${apiUrl}/oauth`,
  redirectUri: `${ownUrl}/oauthcallback`,
Sandro Lutz's avatar
Sandro Lutz committed
function resetSession() {
  APISession.authenticated = false;
  APISession.token = '';
  localStorage.remove('token');
  window.location.replace(oauth.token.getUri());
function checkToken(token) {
  // check if a token is still valid
  return new Promise((resolve, reject) => {
Hermann's avatar
Hermann committed
    amivapi.get(`sessions/${token}`, {
Hermann's avatar
Hermann committed
      headers: { 'Content-Type': 'application/json', Authorization: token },
    }).then((response) => {
      if (response.status === 200) resolve(response.data);
      else reject();
    }).catch(reject);
  });
}

export function checkAuthenticated() {
  // return a promise that resolves always, with a bool that shows whether
  // the user is authenticated
  return new Promise((resolve) => {
    if (APISession.authenticated) resolve();
    else {
      // let's see if we have a stored token
      const token = localStorage.get('token');
      console.log(`found this token: ${token}`);
      if (token !== '') {
        // check of token is valid
Hermann's avatar
Hermann committed
        checkToken(token).then((session) => {
          APISession.token = token;
          APISession.authenticated = true;
Hermann's avatar
Hermann committed
          APISession.userID = session.user;
          console.log(APISession);
          resolve();
        }).catch(resetSession);
      } else resetSession();
    }
  });
}

export function getSession() {
  // Promise resolves with authenticated axios-session or fails
  return new Promise((resolve) => {
    checkAuthenticated().then(() => {
      const authenticatedSession = axios.create({
        headers: {
          'Content-Type': 'application/json',
          Authorization: APISession.token,
        },
        validateStatus: () => true,
      });
      resolve(authenticatedSession);
    }).catch(resetSession);
Sandro Lutz's avatar
Sandro Lutz committed
export function deleteSession() {
  return new Promise((resolve, reject) => {
    getSession().then((api) => {
      api.get(`sessions/${APISession.token}`).then((response) => {
        if (response.status === 200) {
          api.delete(
            `sessions/${response.data._id}`,
            { headers: { 'If-Match': response.data._etag } },
          ).then((deleteResponse) => {
            if (deleteResponse.status === 204) {
              resetSession();
              resolve(deleteResponse.data);
            } else reject();
          }).catch(reject);
        } else reject();
      }).catch(reject);
    });
  });
}

Hermann's avatar
Hermann committed
export function getCurrentUser() {
  return APISession.userID;
}

export class ResourceHandler {
Hermann's avatar
Hermann committed
  /* Handler to get and manipulate resource items
   *
   * resource: String of the resource to accessm e,g, 'events'
   * searchKeys: keys in the resource item on which to perform search, i.e.
   *   when search is set, any of these keys may match the search pattern
   *   E.g. for an event, this may be ['title_de', 'title_en', 'location']
   */
  constructor(resource, searchKeys = false) {
    this.resource = resource;
    this.rights = [];
    // special case for users
    if (resource === 'users') this.searchKeys = ['firstname', 'lastname', 'nethz'];
    else this.searchKeys = searchKeys || config[resource].searchKeys;
    this.noPatchKeys = [
      '_etag', '_id', '_created', '_links', '_updated',
      ...(config[resource].notPatchableKeys || [])];
    checkAuthenticated().then(() => {
      // again special case for users
      if (resource === 'users' && APISession.isUserAdmin) {
        this.searchKeys = searchKeys || config[resource].searchKeys;
      }
    });
Hermann's avatar
Hermann committed
  /*
   * query is a dictionary of different queries
   * Additional to anything specified from eve
   * (http://python-eve.org/features.html#filtering)
   * we support the key `search`, which is translated into a `where` filter
   */
  buildQuerystring(query) {
    const queryKeys = Object.keys(query);

    if (queryKeys.length === 0) return '';

    const fullQuery = {};

    if ('search' in query && query.search && query.search.length > 0 && this.searchKeys) {
      // translate search into where, we just look if any field contains search
Hermann's avatar
Hermann committed
      // The search-string may match any of the keys in the object specified in the
      // constructor
      const searchQuery = {
        $or: this.searchKeys.map((key) => {
          const fieldQuery = {};
          fieldQuery[key] = {
            $regex: `${query.search}`,
            $options: 'i',
          return fieldQuery;
        }),
      };

      // if there exists already a where-filter, AND them together
      if ('where' in query) {
        fullQuery.where = JSON.stringify({ $and: [searchQuery, query.where] });
      } else {
        fullQuery.where = JSON.stringify(searchQuery);
      }
    } else if ('where' in query) {
Hermann's avatar
Hermann committed
      fullQuery.where = JSON.stringify(query.where);
    }

    // add all other keys
    queryKeys.filter(key => (key !== 'where' && key !== 'search'))
      .forEach((key) => { fullQuery[key] = JSON.stringify(query[key]); });

    // now we can acutally build the query string
    return `?${m.buildQueryString(fullQuery)}`;
  }

  networkError(e) {
    console.log(e);
    Snackbar.show({ title: 'Network error, try again.', style: { color: 'red' } });
  }

Hermann's avatar
Hermann committed
  // in future, we may communicate based on the data available
  // therefore, require data already here
  // eslint-disable-next-line no-unused-vars
  error422(data) {
    Snackbar.show({ title: 'Errors in object, please fix.' });
  }

  successful(title) {
    Snackbar.show({ title, style: { color: 'green' } });
  }

  get(query) {
    return new Promise((resolve, reject) => {
      getSession().then((api) => {
        let url = this.resource;
        if (Object.keys(query).length > 0) url += this.buildQuerystring(query);
        api.get(url).then((response) => {
          if (response.status >= 400) {
            resetSession();
            Snackbar.show({ title: response.data, style: { color: 'red' } });
            this.rights = response.data._links.self.methods;
            resolve(response.data);
          }
        }).catch((e) => {
          this.networkError(e);
          reject(e);
        });
      });
    });
  }

  getItem(id, embedded = {}) {
    return new Promise((resolve, reject) => {
      getSession().then((api) => {
        let url = `${this.resource}/${id}`;
        // in case embedded is specified, append to url
        if (Object.keys(embedded).length > 0) {
          url += `?${m.buildQueryString({
Hermann's avatar
Hermann committed
            embedded: JSON.stringify(embedded),
          })}`;
        }
        api.get(url).then((response) => {
          if (response.status >= 400) {
            Snackbar.show({ title: response.data, style: { color: 'red' } });
            resetSession();
            reject();
          } else {
            resolve(response.data);
          }
        }).catch((e) => {
          this.networkError(e);
          reject(e);
        });
      });
    });
  }

  post(item) {
    return new Promise((resolve, reject) => {
      getSession().then((api) => {
        api.post(this.resource, item).then((response) => {
          if (response.code === 201) {
            this.successful('Creation successful.');
            resolve({});
          } else if (response.status === 422) {
            this.error422(response.data);
            reject(response.data);
          } else if (response.status >= 400) {
            Snackbar.show({ title: response.data, style: { color: 'red' } });
            resetSession();
            reject();
          } else {
            resolve(response.data);
          }
        }).catch((e) => {
          this.networkError(e);
  patch(item, formData = false) {
    return new Promise((resolve, reject) => {
      getSession().then((api) => {
        // not all fields in the item can be patched. We filter out the fields
        // we are allowed to send
        let submitData;
        if (formData) {
          submitData = new FormData();
          Object.keys(item).forEach((key) => {
            if (!this.noPatchKeys.includes(key)) {
              submitData.append(key, item[key]);
            }
          });
        } else {
          submitData = Object.assign({}, item);
          this.noPatchKeys.forEach((key) => { delete submitData[key]; });
        }
        api.patch(`${this.resource}/${item._id}`, submitData, {
          headers: { 'If-Match': item._etag },
        }).then((response) => {
          if (response.status === 422) {
            this.error422(response.data);
            reject(response.data);
          } else if (response.status >= 400) {
            Snackbar.show({ title: response.data, style: { color: 'red' } });
            resetSession();
            reject();
          } else {
            this.successful('Change successful.');
            resolve(response.data);
          }
        }).catch((e) => {
          this.networkError(e);
          reject(e);
        });
      });
    });
  }

  delete(item) {
    return new Promise((resolve, reject) => {
      getSession().then((api) => {
        api.delete(`${this.resource}/${item._id}`, {
          headers: { 'If-Match': item._etag },
        }).then((response) => {
          if (response.status >= 400) {
            Snackbar.show({ title: response.data, style: { color: 'red' } });
            resetSession();
            reject();
          } else {
            this.successful('Delete successful.');
            resolve();
          }
        }).catch((e) => {
          this.networkError(e);

export class OauthRedirect {
  view() {
Hermann's avatar
Hermann committed
    oauth.token.getToken(m.route.get()).then((auth) => {
      APISession.authenticated = true;
Hermann's avatar
Hermann committed
      APISession.token = auth.accessToken;
      localStorage.set('token', auth.accessToken);
      amivapi.get(`sessions/${auth.accessToken}`, {
Hermann's avatar
Hermann committed
        headers: { 'Content-Type': 'application/json', Authorization: APISession.token },
      }).then((response) => {
        console.log(response);
        APISession.userID = response.data.user;
        m.route.set('/');
      }).catch(() => {
        resetSession();
      });
    });
    return 'redirecting...';
  }
}