Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • maspect/amiv-admintool
  • emustafa/amiv-admintool
  • dvruette/amiv-admintool
  • amiv/amiv-admintool
4 results
Show changes
Commits on Source (308)
Showing with 18839 additions and 281 deletions
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
{
"name": "amiv-admintool",
"image": "mcr.microsoft.com/vscode/devcontainers/javascript-node:14",
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [9000],
// Uncomment the next line to run commands after the container is created.
"postCreateCommand": "npm install"
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
//"remoteUser": "node"
}
...@@ -7,10 +7,18 @@ module.exports = { ...@@ -7,10 +7,18 @@ module.exports = {
"browser": true, "browser": true,
}, },
"rules": { "rules": {
"no-console": 0, "no-console": 1,
"class-methods-use-this": 0, "class-methods-use-this": 0,
"prefer-destructuring": 1, "prefer-destructuring": 1,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"linebreak-style": 0, "linebreak-style": 0,
"import/no-unresolved": [ "error", { "ignore": [ 'networkConfig' ] } ], // hack until resolving import properly
"object-curly-newline": [ "error", {
ObjectExpression: { multiline: true, consistent: true },
ObjectPattern: { multiline: true, consistent: true },
ImportDeclaration: { minProperties: 7, consistent: true },
ExportDeclaration: { minProperties: 7, consistent: true },
}],
"max-len": [ "error", { "code": 100, ignorePattern: ".*<svg.+>" }],
} }
}; };
.ftpconfig .ftpconfig
node_modules/ node_modules/
package-lock.json
.idea/ .idea/
dist/ dist/
\ No newline at end of file
.DS_Store
stages: stages:
- test
- build - build
- deploy - deploy
build_master_dev: eslint:
stage: build stage: test
image: docker:latest image: node:latest
services:
- docker:dind
before_script: before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - npm install
script: script:
- export BUILD_CONFIG_POSTFIX="dev" - npm run lint
- docker build --pull -t "$CI_REGISTRY_IMAGE:dev" ./
- docker push "$CI_REGISTRY_IMAGE:dev"
only:
- master
build_master_prod: build_master:
stage: build stage: build
image: docker:latest image: docker:stable
services: services:
- docker:dind - docker:dind
before_script: before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - echo "$CI_DOCKER_REGISTRY_TOKEN" | docker login -u "$CI_DOCKER_REGISTRY_USER" --password-stdin
script: script:
- export BUILD_CONFIG_POSTFIX="prod" - docker build --build-arg NPM_BUILD_COMMAND=build --pull -t "$CI_REGISTRY_IMAGE:prod" ./
- docker build --pull -t "$CI_REGISTRY_IMAGE" ./ - docker build --build-arg NPM_BUILD_COMMAND=build-staging --pull -t "$CI_REGISTRY_IMAGE:staging" ./
- docker push "$CI_REGISTRY_IMAGE" - docker build --build-arg NPM_BUILD_COMMAND=build-local --pull -t "$CI_REGISTRY_IMAGE:local" ./
- docker push "$CI_REGISTRY_IMAGE:prod"
- docker push "$CI_REGISTRY_IMAGE:staging"
- docker push "$CI_REGISTRY_IMAGE:local"
environment:
name: production
url: https://admin.amiv.ethz.ch
only: only:
- master - master
# On branches except master: verify that build works, do not push to registry build_dev:
build:
stage: build stage: build
image: docker:latest image: docker:stable
services: services:
- docker:dind - docker:dind
before_script:
- echo "$CI_DOCKER_REGISTRY_TOKEN_DEV" | docker login -u "$CI_DOCKER_REGISTRY_USER_DEV" --password-stdin
script: script:
- docker build --pull ./ - docker build --build-arg NPM_BUILD_COMMAND=build-dev --pull -t "$CI_REGISTRY_IMAGE_DEV" ./
except: - docker push "$CI_REGISTRY_IMAGE_DEV"
- master environment:
name: development
deploy_prod: url: https://admin-dev.amiv.ethz.ch
stage: deploy
image: amiveth/service-update-helper
script:
- /update.py
only:
- master
deploy_dev:
stage: deploy
image: amiveth/service-update-helper
script:
- export CI_DEPLOY_SERVICE="$CI_DEPLOY_SERVICE_DEV"
- /update.py
only:
- master
deploy_prod: deploy:
stage: deploy stage: deploy
image: amiveth/service-update-helper image: amiveth/ansible-ci-helper
script: script:
- export CI_DEPLOY_SERVICE="$CI_DEPLOY_SERVICE_PROD" - python /main.py
- /update.py
only:
- master
# Summary
> Hi! Thanks for contributing to the admintools by reporting a new Bug!
> In order to fix the bug soon, please help us to figure out what causes the
> malfunction!
> describe the issue here
# Steps to Reproduce
> What is the main menu point (left menu bar) where this issue occurs?
> Copy and Paste the url from the window where the issue occured
> Describe in your own words what steps you did before the issue occured
# Additional Debug Information
> Please add information on what browser and operating system you are using
> If possible, can you make a screenshot of the bug?
> Please open the "developer tools" of your browser and copy and paste any
> printouts in the "console"
/label ~bug
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/src/relationlistcontroller.js"
}
]
}
\ No newline at end of file
# First stage: Build project # First stage: Build project
FROM node as build FROM node:14 as build
ARG NPM_BUILD_COMMAND=build
# Copy files and install dependencies # Copy files and install dependencies
COPY ./ / COPY ./ /
RUN npm install RUN npm install
# Build project # Build project
RUN npm run build RUN npm run $NPM_BUILD_COMMAND
# Second stage: Server to deliver files # Second stage: Server to deliver files
FROM node:alpine FROM nginx:1.15-alpine
# Port 8080 can be used as non root
EXPOSE 8080
# Create user with home directory and no password
RUN adduser -Dh /admintool admintool
USER admintool
WORKDIR /admintool
# Install http server
RUN npm install --no-save http-server
# Copy files from first stage # Copy files from first stage
COPY --from=build /index.html /admintool/ COPY --from=build /index.html /var/www/
COPY --from=build /dist /admintool/dist COPY --from=build /dist /var/www/dist
# Run server (-g will automatically serve the gzipped files if possible) # Copy nginx configuration
CMD ["/admintool/node_modules/.bin/http-server", "-g", "/admintool"] COPY nginx.conf /etc/nginx/conf.d/default.conf
# Admintool # Admintool
# Developer Installation # Developer Installation
## w/ Docker
Start the dev Container in VS Code
run ```npm run start``` inside the container.
The development server is available under localhost:9000. It refreshes automatically as soon as you save changes in any `.js` file.
## w/o Docker
``` ```
npm install npm install
...@@ -12,6 +20,20 @@ And now, start developing: ...@@ -12,6 +20,20 @@ And now, start developing:
npm run start npm run start
``` ```
*Warning*: For installation on Ubuntu 16.04 (and possibly similar), you need to install nodejs from the repos source.
1. Remove nodejs if you already have it (ONLY IF YOU REALLY WANT!):
```
sudo apt remove nodejs
```
2. Add nodejs10 from repo (download): `curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -`
3. Install: `sudo apt install -y nodejs`
4. Clean-up and install the packages for amiv-admintools
```
rm -rf node_modules/
npm install
npm run start
```
This will open up a local server outputting the current version of the admintools. It refreshes automatically as soon as you save changes in any `.js` file. This will open up a local server outputting the current version of the admintools. It refreshes automatically as soon as you save changes in any `.js` file.
### File Structure: ### File Structure:
......
...@@ -21,6 +21,18 @@ ...@@ -21,6 +21,18 @@
<link rel="icon" type="image/png" sizes="16x16" href="res/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="res/favicon/favicon-16x16.png">
<!--link href="lib/cust/main.css" rel="stylesheet"--> <!--link href="lib/cust/main.css" rel="stylesheet"-->
<style>
@keyframes spin {
from { transform:rotate(0deg); }
to { transform:rotate(360deg); }
}
@keyframes popup {
from { opacity: 0; }
90% { opacity: 0; }
to { opacity: 100%; }
}
} </style>
</head> </head>
<body> <body>
......
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
root /var/www/;
index index.html;
try_files $uri /index.html =404;
}
This diff is collapsed.
...@@ -6,7 +6,10 @@ ...@@ -6,7 +6,10 @@
"scripts": { "scripts": {
"start": "webpack-dev-server --hot --inline", "start": "webpack-dev-server --hot --inline",
"build": "webpack -p --config webpack.config.prod.js", "build": "webpack -p --config webpack.config.prod.js",
"lint": "eslint src/**" "build-dev": "webpack -p --config webpack.config.dev.js",
"build-staging": "webpack -p --config webpack.config.staging.js",
"build-local": "webpack -p --config webpack.config.local.js",
"lint": "eslint src/**/*.js src/*.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
...@@ -15,31 +18,38 @@ ...@@ -15,31 +18,38 @@
"author": "Hermann Blum et al", "author": "Hermann Blum et al",
"dependencies": { "dependencies": {
"@material/drawer": "^0.30.0", "@material/drawer": "^0.30.0",
"@material/select": "^0.35.1",
"ajv": "^5.5.0", "ajv": "^5.5.0",
"amiv-web-ui-components": "git+https://git@gitlab.ethz.ch/amiv/web-ui-components.git#360e65da63f4511db1f6ac56ce103be9254d1d9f",
"axios": "^0.17.1", "axios": "^0.17.1",
"client-oauth2": "^4.2.0", "client-oauth2": "^4.2.0",
"mithril": "^1.1.6", "mithril": "^1.1.6",
"mithril-infinite": "^1.2.0", "mithril-infinite": "^1.2.4",
"polythene-core-css": "^1.0.0", "polythene-core-css": "^1.2.0",
"polythene-css": "^1.0.0", "polythene-css": "^1.2.0",
"polythene-mithril": "^1.0.0", "polythene-mithril": "1.2.0",
"querystring": "^0.2.0" "querystring": "^0.2.0",
"showdown": "^1.9.0"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.26.0", "@babel/cli": "^7.2.3",
"babel-core": "^6.26.0", "@babel/core": "^7.2.2",
"babel-loader": "^7.1.2", "@babel/plugin-proposal-object-rest-spread": "^7.2.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"babel-preset-env": "^1.6.1", "@babel/preset-env": "^7.2.3",
"compression-webpack-plugin": "^1.1.11", "babel-eslint": "^10.0.1",
"css-loader": "^0.28.7", "babel-loader": "^8.0.4",
"eslint": "^4.10.0", "compression-webpack-plugin": "^2.0.0",
"eslint-config-airbnb-base": "^12.1.0", "css-loader": "^2.1.0",
"eslint-loader": "^1.9.0", "eslint": "^5.16.0",
"eslint-plugin-import": "^2.9.0", "eslint-config-airbnb-base": "^13.1.0",
"file-loader": "^1.1.5", "eslint-import-resolver-webpack": "^0.10.1",
"style-loader": "^0.19.0", "eslint-loader": "^3.0.0",
"webpack": "^3.8.1", "eslint-plugin-import": "^2.14.0",
"webpack-dev-server": "^2.9.5" "file-loader": "^3.0.1",
"style-loader": "^0.23.1",
"webpack": "^4.28.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14"
} }
} }
import tool from 'announcetool';
const m = require('mithril');
export default class AnnounceTool {
oncreate() {
if (tool.wasRenderedOnce()) {
// jQuery catches the first document.ready, but afterwards we have to
// trigger a render
tool.render();
}
}
view() {
return m('div', [
m('div#tableset', [
m('p#events'),
m('div#buttonrow', [
m('button#preview.btn.btn-default', 'Preview'),
m('button#reset.btn.btn-default', 'Reset'),
m('button#send.btn.btn-default', 'Send'),
]),
]),
m('br'),
m('hr'),
m('textarea#target'),
]);
}
}
import m from 'mithril'; import m from 'mithril';
import axios from 'axios'; import axios from 'axios';
import ClientOAuth2 from 'client-oauth2'; import ClientOAuth2 from 'client-oauth2';
import { Snackbar } from 'polythene-mithril';
// eslint-disable-next-line import/extensions
import { apiUrl, ownUrl, oAuthID } from 'networkConfig'; import { apiUrl, ownUrl, oAuthID } from 'networkConfig';
import * as localStorage from './localStorage'; import * as localStorage from './localStorage';
import config from './resourceConfig.json';
// Object which stores the current login-state // Object which stores the current login-state
const APISession = { const APISession = {
authenticated: false, authenticated: false,
token: '', token: '',
userID: null,
schema: null,
rights: {
users: [],
joboffers: [],
studydocuments: [],
},
}; };
if (!APISession.schema) {
m.request(`${apiUrl}/docs/api-docs`).then((schema) => { APISession.schema = schema; });
}
const amivapi = axios.create({
baseURL: apiUrl,
headers: { 'Content-Type': 'application/json' },
validateStatus: () => true,
});
// OAuth Handler // OAuth Handler
const oauth = new ClientOAuth2({ const oauth = new ClientOAuth2({
clientId: oAuthID, clientId: oAuthID,
...@@ -18,30 +36,20 @@ const oauth = new ClientOAuth2({ ...@@ -18,30 +36,20 @@ const oauth = new ClientOAuth2({
redirectUri: `${ownUrl}/oauthcallback`, redirectUri: `${ownUrl}/oauthcallback`,
}); });
export function resetSession() { function resetSession() {
APISession.authenticated = false; APISession.authenticated = false;
APISession.token = ''; APISession.token = '';
localStorage.remove('token'); localStorage.remove('token');
window.location.replace(oauth.token.getUri()); window.location.replace(oauth.token.getUri());
} }
const amivapi = axios.create({
baseURL: apiUrl,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
function checkToken(token) { function checkToken(token) {
// check if a token is still valid // check if a token is still valid
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
amivapi.get('users', { amivapi.get(`sessions/${token}`, {
headers: { headers: { 'Content-Type': 'application/json', Authorization: token },
'Content-Type': 'application/json',
Authorization: token,
},
}).then((response) => { }).then((response) => {
console.log(response.data); if (response.status === 200) resolve(response.data);
if (response.status === 200) resolve();
else reject(); else reject();
}).catch(reject); }).catch(reject);
}); });
...@@ -53,16 +61,24 @@ export function checkAuthenticated() { ...@@ -53,16 +61,24 @@ export function checkAuthenticated() {
return new Promise((resolve) => { return new Promise((resolve) => {
if (APISession.authenticated) resolve(); if (APISession.authenticated) resolve();
else { else {
console.log('looking for token');
// let's see if we have a stored token // let's see if we have a stored token
const token = localStorage.get('token'); const token = localStorage.get('token');
console.log(`found this token: ${token}`);
if (token !== '') { if (token !== '') {
// check of token is valid // check of token is valid
checkToken(token).then(() => { checkToken(token).then((session) => {
APISession.token = token; APISession.token = token;
APISession.authenticated = true; APISession.authenticated = true;
resolve(); APISession.userID = session.user;
amivapi.get('/', {
headers: { 'Content-Type': 'application/json', Authorization: token },
}).then((response) => {
const rights = {};
response.data._links.child.forEach(({ href, methods }) => {
rights[href] = methods;
});
APISession.rights = rights;
resolve();
});
}).catch(resetSession); }).catch(resetSession);
} else resetSession(); } else resetSession();
} }
...@@ -75,28 +91,106 @@ export function getSession() { ...@@ -75,28 +91,106 @@ export function getSession() {
checkAuthenticated().then(() => { checkAuthenticated().then(() => {
const authenticatedSession = axios.create({ const authenticatedSession = axios.create({
baseURL: apiUrl, baseURL: apiUrl,
timeout: 10000,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: APISession.token, Authorization: APISession.token,
}, },
validateStatus: () => true,
}); });
resolve(authenticatedSession); resolve(authenticatedSession);
}).catch(resetSession); }).catch(resetSession);
}); });
} }
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);
});
});
}
export function getCurrentUser() {
return APISession.userID;
}
export function getUserRights() {
return APISession.rights;
}
export function getSchema() {
return APISession.schema;
}
// Mapper for resource vs schema-object names
const objectNameForResource = {
users: 'User',
groupmemberships: 'Group Membership',
groups: 'Group',
eventsignups: 'Event Signup',
events: 'Event',
studydocuments: 'Study Document',
joboffers: 'Job Offer',
blacklist: 'Blacklist',
sessions: 'Session',
};
export class ResourceHandler { export class ResourceHandler {
constructor(resource, searchKeys = false) { /* 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 = []) {
// backwards compatibiliity, should be removed in future
// eslint-disable-next-line no-param-reassign
if (!searchKeys) searchKeys = [];
this.resource = resource; this.resource = resource;
this.searchKeys = searchKeys || config[resource].searchKeys; this.rights = [];
this.noPatchKeys = [ this.schema = JSON.parse(JSON.stringify(getSchema().components.schemas[
'_etag', '_id', '_created', '_links', '_updated', objectNameForResource[this.resource]]));
...(config[resource].notPatchableKeys || [])]; // readOnly fields should be removed before patch
checkAuthenticated(); this.noPatchKeys = Object.keys(this.schema.properties).filter(
key => this.schema.properties[key].readOnly,
);
// any field that is a string can be searched
const possibleSearchKeys = Object.keys(this.schema.properties).filter((key) => {
const field = this.schema.properties[key];
return field.type === 'string' && field.format !== 'objectid'
&& field.format !== 'date-time' && !key.startsWith('_');
});
// special case for users, we don't allow reverse search by legi or rfid
if (resource === 'users') this.searchKeys = ['firstname', 'lastname', 'nethz'];
else this.searchKeys = searchKeys.length === 0 ? possibleSearchKeys : searchKeys;
checkAuthenticated().then(() => {
// again special case for users
if (resource === 'users' && APISession.isUserAdmin) {
this.searchKeys = searchKeys.length === 0 ? possibleSearchKeys : searchKeys;
}
});
} }
// definitions of query parameters in addition to API go here /*
* 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) { buildQuerystring(query) {
const queryKeys = Object.keys(query); const queryKeys = Object.keys(query);
...@@ -104,15 +198,38 @@ export class ResourceHandler { ...@@ -104,15 +198,38 @@ export class ResourceHandler {
const fullQuery = {}; const fullQuery = {};
if ('search' in query && query.search.length > 0) { if ('search' in query && query.search && query.search.length > 0
&& this.searchKeys.length !== 0) {
// translate search into where, we just look if any field contains search // translate search into where, we just look if any field contains search
const searchQuery = { // The search-string may match any of the keys in the object specified in the
$or: this.searchKeys.map((key) => { // constructor
const fieldQuery = {}; let searchQuery;
fieldQuery[key] = query.search; if (query.search.split(' ').length > 1) {
return fieldQuery; searchQuery = {
}), $and: query.search.split(' ').map(searchPhrase => ({
}; $or: this.searchKeys.map((key) => {
const fieldQuery = {};
fieldQuery[key] = {
$regex: `${searchPhrase}`,
$options: 'i',
};
return fieldQuery;
}),
})),
};
} else {
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 there exists already a where-filter, AND them together
if ('where' in query) { if ('where' in query) {
...@@ -120,7 +237,7 @@ export class ResourceHandler { ...@@ -120,7 +237,7 @@ export class ResourceHandler {
} else { } else {
fullQuery.where = JSON.stringify(searchQuery); fullQuery.where = JSON.stringify(searchQuery);
} }
} else if (query.where) { } else if ('where' in query) {
fullQuery.where = JSON.stringify(query.where); fullQuery.where = JSON.stringify(query.where);
} }
...@@ -129,7 +246,25 @@ export class ResourceHandler { ...@@ -129,7 +246,25 @@ export class ResourceHandler {
.forEach((key) => { fullQuery[key] = JSON.stringify(query[key]); }); .forEach((key) => { fullQuery[key] = JSON.stringify(query[key]); });
// now we can acutally build the query string // now we can acutally build the query string
return `?${m.buildQueryString(fullQuery)}`; const maxResults = 50; // max_results for setting the page size of the return.
return `?${m.buildQueryString(fullQuery)}&max_results=${maxResults}`;
}
networkError(e) {
// eslint-disable-next-line no-console
console.log(e);
Snackbar.show({ title: 'Network error, try again.', style: { color: 'red' } });
}
// 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) { get(query) {
...@@ -139,13 +274,15 @@ export class ResourceHandler { ...@@ -139,13 +274,15 @@ export class ResourceHandler {
if (Object.keys(query).length > 0) url += this.buildQuerystring(query); if (Object.keys(query).length > 0) url += this.buildQuerystring(query);
api.get(url).then((response) => { api.get(url).then((response) => {
if (response.status >= 400) { if (response.status >= 400) {
resetSession(); Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject(); reject();
} else { } else {
this.rights = response.data._links.self.methods;
resolve(response.data); resolve(response.data);
} }
}).catch((e) => { }).catch((e) => {
console.log(e); this.networkError(e);
reject(e); reject(e);
}); });
}); });
...@@ -159,18 +296,21 @@ export class ResourceHandler { ...@@ -159,18 +296,21 @@ export class ResourceHandler {
// in case embedded is specified, append to url // in case embedded is specified, append to url
if (Object.keys(embedded).length > 0) { if (Object.keys(embedded).length > 0) {
url += `?${m.buildQueryString({ url += `?${m.buildQueryString({
embedded: JSON.stringify(this.embedded), embedded: JSON.stringify(embedded),
})}`; })}`;
} }
api.get(url).then((response) => { api.get(url).then((response) => {
if (response.status >= 400) { if (response.status === 404) {
resetSession(); m.route.set('/404');
} else if (response.status >= 400) {
Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject(); reject();
} else { } else {
resolve(response.data); resolve(response.data);
} }
}).catch((e) => { }).catch((e) => {
console.log(e); this.networkError(e);
reject(e); reject(e);
}); });
}); });
...@@ -182,54 +322,58 @@ export class ResourceHandler { ...@@ -182,54 +322,58 @@ export class ResourceHandler {
getSession().then((api) => { getSession().then((api) => {
api.post(this.resource, item).then((response) => { api.post(this.resource, item).then((response) => {
if (response.code === 201) { if (response.code === 201) {
this.successful('Creation successful.');
resolve({}); resolve({});
} else if (response.status === 422) { } else if (response.status === 422) {
this.error422(response.data);
reject(response.data); reject(response.data);
} else if (response.status >= 400) { } else if (response.status >= 400) {
resetSession(); Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject(); reject();
} else { } else {
resolve(response.data); resolve(response.data);
} }
}).catch((e) => { }).catch((e) => {
console.log(e); this.networkError(e);
reject(e); reject(e);
}); });
}); });
}); });
} }
patch(item, formData = false) { patch(item) {
const isFormData = item instanceof FormData;
let patchInfo = {};
if (isFormData) patchInfo = { id: item.get('_id'), etag: item.get('_etag') };
else patchInfo = { id: item._id, etag: item._etag };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getSession().then((api) => { getSession().then((api) => {
let submitData = item;
if (!isFormData) submitData = Object.assign({}, item);
// not all fields in the item can be patched. We filter out the fields // not all fields in the item can be patched. We filter out the fields
// we are allowed to send // we are allowed to send
let submitData; this.noPatchKeys.forEach((key) => {
if (formData) { if (isFormData) submitData.delete(key);
submitData = new FormData(); else delete submitData[key];
Object.keys(item).forEach((key) => { });
if (!this.noPatchKeys.includes(key)) { api.patch(`${this.resource}/${patchInfo.id}`, submitData, {
submitData.append(key, item[key]); headers: { 'If-Match': patchInfo.etag },
}
});
} 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) => { }).then((response) => {
if (response.status === 422) { if (response.status === 422) {
this.error422(response.data);
reject(response.data); reject(response.data);
} else if (response.status >= 400) { } else if (response.status >= 400) {
resetSession(); Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject(); reject();
} else { } else {
this.successful('Change successful.');
resolve(response.data); resolve(response.data);
} }
}).catch((e) => { }).catch((e) => {
console.log(e); this.networkError(e);
reject(e); reject(e);
}); });
}); });
...@@ -243,13 +387,15 @@ export class ResourceHandler { ...@@ -243,13 +387,15 @@ export class ResourceHandler {
headers: { 'If-Match': item._etag }, headers: { 'If-Match': item._etag },
}).then((response) => { }).then((response) => {
if (response.status >= 400) { if (response.status >= 400) {
resetSession(); Snackbar.show({ title: response.data, style: { color: 'red' } });
if (response.status === 401) resetSession();
reject(); reject();
} else { } else {
this.successful('Delete successful.');
resolve(); resolve();
} }
}).catch((e) => { }).catch((e) => {
console.log(e); this.networkError(e);
reject(e); reject(e);
}); });
}); });
...@@ -259,11 +405,13 @@ export class ResourceHandler { ...@@ -259,11 +405,13 @@ export class ResourceHandler {
export class OauthRedirect { export class OauthRedirect {
view() { view() {
oauth.token.getToken(m.route.get()).then((response) => { oauth.token.getToken(m.route.get()).then((auth) => {
APISession.authenticated = true; localStorage.set('token', auth.accessToken);
APISession.token = response.accessToken; checkAuthenticated().then(() => {
localStorage.set('token', response.accessToken); // checkAuthenticated will check whetehr the token is valid
m.route.set('/users'); // and store all relevant session info for easy access
m.route.set('/');
}).catch(resetSession);
}); });
return 'redirecting...'; return 'redirecting...';
} }
......
/* eslint-disable operator-assignment */
/* eslint-disable no-restricted-globals */
/* eslint-disable no-bitwise */
/* eslint-disable no-plusplus */
/* eslint-disable no-param-reassign */
/**
*
* Base64 encode / decode
* http://www.webtoolkit.info
*
* */
const Base64 = {
// private property
_keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
// public method for encoding
encode(input) {
let output = '';
let chr1; let chr2; let chr3; let enc1; let enc2; let enc3; let enc4;
let i = 0;
input = Base64._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = 64;
enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output
+ this._keyStr.charAt(enc1)
+ this._keyStr.charAt(enc2)
+ this._keyStr.charAt(enc3)
+ this._keyStr.charAt(enc4);
} // Whend
return output;
}, // End Function encode
// public method for decoding
decode(input) {
let output = '';
let chr1; let chr2; let chr3;
let enc1; let enc2; let enc3; let enc4;
let i = 0;
// eslint-disable-next-line no-useless-escape
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '');
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
// eslint-disable-next-line eqeqeq
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
// eslint-disable-next-line eqeqeq
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
} // Whend
output = Base64._utf8_decode(output);
return output;
}, // End Function decode
// private method for UTF-8 encoding
_utf8_encode(string) {
let utftext = '';
string = string.replace(/\r\n/g, '\n');
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
} else if (c > 127 && c < 2048) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
} else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
} // Next n
return utftext;
}, // End Function _utf8_encode
// private method for UTF-8 decoding
_utf8_decode(utftext) {
let string = '';
let i = 0;
let c; let c2; let c3;
c = 0;
c2 = 0;
while (i < utftext.length) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
} else if (c > 191 && c < 224) {
c2 = utftext.charCodeAt(i + 1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
} else {
c2 = utftext.charCodeAt(i + 1);
c3 = utftext.charCodeAt(i + 2);
string += String.fromCharCode(
((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63),
);
i += 3;
}
} // Whend
return string;
}, // End Function _utf8_decode
};
export default Base64;
import m from 'mithril';
import { TextField } from 'polythene-mithril';
import { ListSelect, DatalistController } from 'amiv-web-ui-components';
import { ResourceHandler } from '../auth';
import EditView from '../views/editView';
class NanoController {
constructor(resource) {
this.resource = resource;
this.handler = new ResourceHandler(resource);
this.data = {};
}
post(data) {
return new Promise((resolve, reject) => {
this.handler.post(data).then(() => {
this.cancel();
}).catch(reject);
});
}
cancel() {
m.route.set(`/${this.resource}`);
}
}
/**
* Table of all possible permissions to edit
*
* @class PermissionEditor (name)
*/
export default class NewBlacklist extends EditView {
constructor({ attrs }) {
super({ attrs: { controller: new NanoController('blacklist'), ...attrs } });
this.userHandler = new ResourceHandler('users', ['firstname', 'lastname', 'email', 'nethz']);
this.userController = new DatalistController((query, search) => this.userHandler.get(
{ search, ...query },
));
}
beforeSubmit() {
const { data } = this.form;
// exchange user object with string of id
this.submit({ ...data, user: data.user ? data.user._id : undefined }).then(() => {
this.controller.changeModus('view');
});
}
view() {
return this.layout([
m('div', { style: { display: 'flex' } }, [
m(TextField, { label: 'User: ', disabled: true, style: { width: '160px' } }),
m('div', { style: { 'flex-grow': 1 } }, m(ListSelect, {
controller: this.userController,
selection: this.form.data.user,
listTileAttrs: user => Object.assign({}, { title: `${user.firstname} ${user.lastname}` }),
selectedText: user => `${user.firstname} ${user.lastname}`,
onSelect: (data) => { this.form.data.user = data; },
})),
]),
...this.form.renderSchema(['reason', 'start_time', 'price']),
]);
}
}
import m from 'mithril';
import { Button } from 'polythene-mithril';
import TableView from '../views/tableView';
import { ResourceHandler } from '../auth';
import RelationlistController from '../relationlistcontroller';
import { dateFormatter } from '../utils';
export default class BlacklistTable {
constructor() {
this.handler = new ResourceHandler('blacklist');
this.ctrl = new RelationlistController({
primary: 'blacklist',
secondary: 'users',
query: { sort: [['start_time', -1]] },
});
}
getItemData(data) {
return [
m(
'div', { style: { width: '18em' } },
m(
'div', { style: { 'font-weight': 'bold' } },
`${data.user.firstname} ${data.user.lastname}`,
),
m('div', data.user.email),
),
m(
'div', { style: { width: 'calc(100%-18em)' } },
m('div', `From ${dateFormatter(data.start_time, false)}
${data.end_time ? ` to ${dateFormatter(data.end_time, false)}` : ''}`),
m('div', `Reason: ${data.reason}`),
data.price && m('div', `price: ${data.price}`),
),
m('div', { style: { 'flex-grow': '100' } }),
m('div', (!data.end_time && this.ctrl.handler.rights.includes('POST')) && m(Button, {
// Button to mark this entry as resolved
className: 'blue-row-button',
borders: false,
label: 'redeem',
events: {
onclick: () => {
const date = new Date(Date.now());
const patchdata = Object.assign({}, data);
delete patchdata.user;
patchdata.end_time = `${date.toISOString().slice(0, -5)}Z`;
this.ctrl.handler.patch(patchdata).then(() => {
this.ctrl.refresh();
m.redraw();
});
},
},
})),
];
}
view() {
return m(TableView, {
clickOnRows: false,
controller: this.ctrl,
keys: [null, null],
tileContent: data => this.getItemData(data),
titles: [
{ text: 'User', width: '18em' },
{ text: 'Detail', width: '9em' },
],
onAdd: (this.ctrl.handler.rights.includes('POST')) ? () => {
m.route.set('/newblacklistentry');
} : false,
});
}
}
{
"apiUrl": "https://api-dev.amiv.ethz.ch/",
"events": {
"keyDescriptors": {
"title_de": "German Title",
"title_en": "English Title",
"location": "Location",
"show_website": "Event is shown on the website",
"priority": "Priority",
"time_end": "Ending time",
"time_register_end": "Deadline for registration",
"time_start": "Starting time",
"spots": "Spots available",
"allow_email_signup": "Event open for non-AMIV members",
"price": "Price",
"signup_count": "Signed-up participants",
"catchphrase_en": "Catchphrase in English. Announce and Website.",
"catchphrase_de": "Schlagwort auf Deutsch",
"description_de": "Beschreibung auf Deutsch",
"description_en": "Description in English",
"img_banner": "Banner as png",
"img_poster": "Poster as png",
"img_thumbnail": "Thumbnail as png",
"show_infoscreen": "Does the event show on the infoscreen?",
"img_infoscreen": "Infoscreen as png",
"time_advertising_end": "Advertisment ends on",
"time_advertising_start": "Advertisement starts on",
"selection_strategy": "TODO what is this?",
"show_announce": "Does it belong to announce?",
"_id": "TODO Event ID how is this generated?"
},
"tableKeys": [
"title_de",
"time_start",
"time_end",
"time_register_end",
"show_website",
"priority"
],
"notPatchableKeys": [
"signup_count"
]
},
"users": {
"keyDescriptors": {
"legi": "Legi Number",
"firstname": "First Name",
"lastname": "Last Name",
"rfid": "RFID",
"phone": "Phone",
"nethz": "nethz Account",
"gender": "Gender",
"department": "Department",
"email": "Email"
},
"tableKeys": [
"firstname",
"lastname",
"nethz",
"legi",
"membership"
],
"searchKeys": [
"firstname",
"lastname",
"nethz",
"legi",
"department"
],
"notPatchableKeys": [
"password_set"
]
},
"groups": {
"keyDescriptors": {
"name": "Name"
},
"searchKeys": ["name"],
"patchableKeys": ["name"]
},
"groupmemberships": {
"patchableKeys": ["user", "group"]
},
"eventsignups": {
"patchableKeys": ["event"],
"tableKeys": [
"_created",
"user.lastname",
"user.firstname",
"email"
],
"searchKeys": []
}
}