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 (208)
Showing with 19322 additions and 556 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,7 +7,7 @@ module.exports = { ...@@ -7,7 +7,7 @@ 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,
......
.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:
- docker build --build-arg NPM_BUILD_COMMAND=build-dev --pull -t "$CI_REGISTRY_IMAGE:dev" ./ - npm run lint
- 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:
- docker build --pull -t "$CI_REGISTRY_IMAGE" ./ - docker build --build-arg NPM_BUILD_COMMAND=build --pull -t "$CI_REGISTRY_IMAGE:prod" ./
- docker push "$CI_REGISTRY_IMAGE" - docker build --build-arg NPM_BUILD_COMMAND=build-staging --pull -t "$CI_REGISTRY_IMAGE:staging" ./
- 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 ARG NPM_BUILD_COMMAND=build
...@@ -12,26 +12,11 @@ RUN npm run $NPM_BUILD_COMMAND ...@@ -12,26 +12,11 @@ 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
# Serve index.html for every file which is not found on the server
# Hotfix for direct links
RUN ln index.html 404.html
# 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:
......
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.
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
"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",
"build-dev": "webpack -p --config webpack.config.dev.js", "build-dev": "webpack -p --config webpack.config.dev.js",
"lint": "eslint src/**" "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",
...@@ -18,30 +20,36 @@ ...@@ -18,30 +20,36 @@
"@material/drawer": "^0.30.0", "@material/drawer": "^0.30.0",
"@material/select": "^0.35.1", "@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.4", "mithril-infinite": "^1.2.4",
"polythene-core-css": "^1.2.0", "polythene-core-css": "^1.2.0",
"polythene-css": "^1.2.0", "polythene-css": "^1.2.0",
"polythene-mithril": "^1.2.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.11", "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: '',
// user admins are a very special case as the permissions on the resource can only userID: null,
// be seen by requesting users and check whether you see their membership schema: null,
isUserAdmin: false 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,
...@@ -21,22 +36,17 @@ const oauth = new ClientOAuth2({ ...@@ -21,22 +36,17 @@ 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,
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: { 'Content-Type': 'application/json', Authorization: token }, headers: { 'Content-Type': 'application/json', Authorization: token },
}).then((response) => { }).then((response) => {
if (response.status === 200) resolve(response.data); if (response.status === 200) resolve(response.data);
...@@ -53,19 +63,22 @@ export function checkAuthenticated() { ...@@ -53,19 +63,22 @@ export function checkAuthenticated() {
else { else {
// 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((users) => { checkToken(token).then((session) => {
APISession.token = token; APISession.token = token;
APISession.authenticated = true; APISession.authenticated = true;
// if we see the membership of more than 1 person in the response, we APISession.userID = session.user;
// have admin rights on users amivapi.get('/', {
if (users._items[0].membership && users._items[1].membership) { headers: { 'Content-Type': 'application/json', Authorization: token },
APISession.isUserAdmin = true; }).then((response) => {
} const rights = {};
console.log(APISession); response.data._links.child.forEach(({ href, methods }) => {
resolve(); rights[href] = methods;
});
APISession.rights = rights;
resolve();
});
}).catch(resetSession); }).catch(resetSession);
} else resetSession(); } else resetSession();
} }
...@@ -82,12 +95,58 @@ export function getSession() { ...@@ -82,12 +95,58 @@ export function getSession() {
'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 {
/* Handler to get and manipulate resource items /* Handler to get and manipulate resource items
* *
...@@ -96,18 +155,32 @@ export class ResourceHandler { ...@@ -96,18 +155,32 @@ export class ResourceHandler {
* when search is set, any of these keys may match the search pattern * 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'] * E.g. for an event, this may be ['title_de', 'title_en', 'location']
*/ */
constructor(resource, searchKeys = false) { 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;
// special case for users this.rights = [];
this.schema = JSON.parse(JSON.stringify(getSchema().components.schemas[
objectNameForResource[this.resource]]));
// readOnly fields should be removed before patch
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']; if (resource === 'users') this.searchKeys = ['firstname', 'lastname', 'nethz'];
else this.searchKeys = searchKeys || config[resource].searchKeys; else this.searchKeys = searchKeys.length === 0 ? possibleSearchKeys : searchKeys;
this.noPatchKeys = [
'_etag', '_id', '_created', '_links', '_updated',
...(config[resource].notPatchableKeys || [])];
checkAuthenticated().then(() => { checkAuthenticated().then(() => {
// again special case for users // again special case for users
if (resource === 'users' && APISession.isUserAdmin) { if (resource === 'users' && APISession.isUserAdmin) {
this.searchKeys = searchKeys || config[resource].searchKeys; this.searchKeys = searchKeys.length === 0 ? possibleSearchKeys : searchKeys;
} }
}); });
} }
...@@ -125,20 +198,38 @@ export class ResourceHandler { ...@@ -125,20 +198,38 @@ export class ResourceHandler {
const fullQuery = {}; const fullQuery = {};
if ('search' in query && query.search && 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
// The search-string may match any of the keys in the object specified in the // The search-string may match any of the keys in the object specified in the
// constructor // constructor
const searchQuery = { let searchQuery;
$or: this.searchKeys.map((key) => { if (query.search.split(' ').length > 1) {
const fieldQuery = {}; searchQuery = {
fieldQuery[key] = { $and: query.search.split(' ').map(searchPhrase => ({
$regex: `${query.search}`, $or: this.searchKeys.map((key) => {
$options: 'i', const fieldQuery = {};
}; fieldQuery[key] = {
return fieldQuery; $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) {
...@@ -155,7 +246,25 @@ export class ResourceHandler { ...@@ -155,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) {
...@@ -165,13 +274,15 @@ export class ResourceHandler { ...@@ -165,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);
}); });
}); });
...@@ -189,14 +300,17 @@ export class ResourceHandler { ...@@ -189,14 +300,17 @@ export class ResourceHandler {
})}`; })}`;
} }
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);
}); });
}); });
...@@ -208,54 +322,58 @@ export class ResourceHandler { ...@@ -208,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);
}); });
}); });
...@@ -269,13 +387,15 @@ export class ResourceHandler { ...@@ -269,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);
}); });
}); });
...@@ -285,11 +405,13 @@ export class ResourceHandler { ...@@ -285,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,
});
}
}
import ItemView from './views/itemView';
import EditView from './views/editView';
import { inputGroup, selectGroup, submitButton } from './views/elements';
import TableView from './views/tableView';
import { events as config } from './config.json';
const m = require('mithril');
export class EventView extends ItemView {
constructor() {
super('events');
this.memberships = [];
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
let comissionBadge = m('span.label.label-important', 'Who is resp of this event?');
if (this.data.membership === 'kultur') {
comissionBadge = m('span.label.label-success', 'Kulturi event');
} else if (this.data.membership === 'eestec') {
comissionBadge = m('span.label.label-important', 'EESTEC event');
} else if (this.data.membership === 'limes') {
comissionBadge = m('span.label.label-warning', 'LIMES event');
}
// TODO Question Lio171201:are we missing a "responsible" key?
const detailKeys = [
'title_de',
'rfid',
'location', 'time_start', 'time_end',
'show_website', 'catchphrase',
'time_register_start', 'price', 'allow_email_signup'];
return m('div', [
m('h1', `${this.data.title_de}`),
comissionBadge,
m('table', detailKeys.map(key => m('tr', [
m('td.detail-descriptor', config.keyDescriptors[key]),
m('td', this.data[key] ? this.data[key] : ''),
]))),
m('h2', 'Location'), m('br'),
m(TableView, {
resource: 'events',
keys: ['event.location'],
query: {
where: { user: this.id },
embedded: { group: 1 },
},
}),
m('h2', 'Signups'), m('br'),
m(TableView, {
resource: 'events',
keys: ['event.title_de'],
query: {
where: { user: this.id },
embedded: { event: 1 },
},
}),
]);
}
}
class EventEdit extends EditView {
constructor(vnode) {
super(vnode, 'events');
}
getForm() {
return m('form', [
m('div.row', [
m(inputGroup, this.bind({ title: 'Deutscher Titel', name: 'title_de' })),
m(inputGroup, this.bind({ title: 'English Title', name: 'title_en' })),
m(inputGroup, this.bind({ title: 'Location', name: 'location' })),
// m(inputGroup, this.bind({ title: 'Date-start', name: 'datetimepicker1' })),
// $('#datetimepicker1').datetimepicker();
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'May non-AMIV members register?',
name: 'allow_email_signup',
options: [true, false],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Show on the website?',
name: 'show_website',
options: [true, false],
})),
m(selectGroup, this.bind({
classes: 'col-xs-6',
title: 'Piority from 1 to 10?',
name: 'priority',
// could be done with array.apply:
options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
})),
]),
m('span', JSON.stringify(this.data)),
m('span', JSON.stringify(this.errors)),
]);
}
view() {
// do not render anything if there is no data yet
if (!this.data) return m.trust('');
return m('form', [
this.getForm(),
m(submitButton, {
active: this.valid,
args: {
onclick: this.submit('PATCH', config.patchableKeys),
class: 'btn-warning',
},
text: 'Update',
}),
]);
}
}
export class NewEvent extends EventEdit {
constructor(vnode) {
super(vnode);
this.data = {
title_de: 'Unvollstaendiges Event',
priority: 7,
show_website: false,
};
this.valid = false;
// if the creation is finished, UI should switch to new Event
this.callback = (response) => { m.route.set(`/events/${response.data._id}`); };
}
view() {
return m('form', [
this.getForm(),
m(submitButton, {
active: this.valid,
args: {
onclick: this.submit('POST', config.patchableKeys),
class: 'btn-warning',
},
text: 'Create',
}),
]);
}
}
export class EventModal {
constructor() {
this.edit = false;
}
view() {
if (this.edit) {
return m(EventEdit, { onfinish: () => { this.edit = false; m.redraw(); } });
}
// else
return m('div', [
m('div.btn.btn-default', { onclick: () => { this.edit = true; } }, 'Edit'),
m('br'),
m(EventView),
]);
}
}
This diff is collapsed.