Commit 0c787016 authored by Petter Goksøyr Åsen's avatar Petter Goksøyr Åsen

Update to state at retirement

parent 695fd5ce
Pipeline #3992 passed with stages
in 2 minutes and 49 seconds
......@@ -3,15 +3,19 @@ FROM node:9.11.1
WORKDIR /usr/src/app
COPY package.json package.json
COPY public public
COPY src/backend backend
COPY src/common common
COPY .eslintrc .eslintrc
RUN npm set progress=false
RUN npm set color=false
RUN npm install > install.log 2>&1
COPY . .
EXPOSE 8000
RUN npm run lint
RUN npm run productionbuild
CMD [ "npm", "run", "production"]
\ No newline at end of file
ENV NODE_ENV production
ARG HISTORY
LABEL no.deichman.label.component-history="${HISTORY}"
CMD [ "node", "src/backend/server.js"]
FROM node:9.11.1
WORKDIR /usr/src/app
COPY package.json package.json
COPY .eslintrc .eslintrc
RUN npm set progress=false
RUN npm set color=false
RUN npm install > install.log 2>&1
COPY . .
RUN npm run productionbuild
EXPOSE 8000
CMD [ "node", "src/backend/server.js"]
......@@ -3,16 +3,13 @@ FROM node:9.11.1
WORKDIR /usr/src/app
COPY package.json package.json
COPY .eslintrc .eslintrc
RUN npm set progress=false
RUN npm set color=false
RUN npm install > install.log 2>&1
COPY . .
VOLUME /usr/src/app/src
VOLUME /usr/src/app/test
EXPOSE 8000
RUN npm run lint
CMD [ "npm", "run", "dockerdev" ]
\ No newline at end of file
export GITREF=$(shell git rev-parse HEAD)
IMAGE=digibib/timber
CONTAINER=timber
TIMBERPATH=$(shell pwd)
HOST_VOLUME_BINDINGS=-v $(TIMBERPATH)/src:/usr/src/app/src \
-v $(TIMBERPATH)/test:/usr/src/app/test
.PHONY: devbuild build test lint logs push
devbuild:
docker stop $(CONTAINER) || true &&\
docker rm -f $(CONTAINER) || true &&\
docker-compose build $(CONTAINER) &&\
docker-compose up --force-recreate --no-deps -d $(CONTAINER)
build:
docker build -t $(IMAGE):$(GITREF) .
docker tag $(IMAGE):$(GITREF) $(IMAGE):latest
lint:
docker run --rm $(HOST_VOLUME_BINDINGS) $(IMAGE):$(GITREF) npm run -s lint
test: lint
docker run --rm $(HOST_VOLUME_BINDINGS) $(IMAGE):$(GITREF) npm test
logs:
docker logs -f $(CONTAINER)
push:
ifndef TAG
@echo "You must specify TAG when pushing"
exit 1
endif
docker push $(IMAGE):$(TAG)
# Timber
Timber is the main interface for patrons to interact with the library on the internet, including searching and browsing the catalogue.
Timber is a browser-based application. It is consuming data from Services and the search index.
## Technologies used
All modules are specified in package.json.
* [Node.JS](https://nodejs.org/)
* Server
* [Express](http://expressjs.com/)
* Templating/Browser rendering
* [React](http://facebook.github.io/react/)
* [React-router](https://github.com/rackt/react-routerq)
* [Redux](https://github.com/rackt/redux)
* Testing
* [Mocha](https://mochajs.org/)
* React-test-utils
* [Jsdom](https://github.com/tmpvar/jsdom)
* Build
* Gulp (watch, uglify, generate sourcemaps, browserify)
## Build
See [Makefile](Makefile).
## Troubleshooting
Logs can be viewed via:
* Running `make logs` in the `/vagrant/redef/timber` (when you have ssh'ed into dev-ship)
---
version: '3'
networks:
default:
external:
name: localhost_backend
services:
timber:
container_name: timber
image: "digibib/timber:${GITREF}"
build:
dockerfile: Dockerfile-dev
context: "."
volumes:
- "./src:/usr/src/app/src"
- "./test:/usr/src/app/test"
environment:
SERVICES_PORT: "http://services:${SERVICES_PORT:-8005}"
KOHA_API_USER: "${KOHA_API_USER:-api}"
KOHA_API_PASS: "${KOHA_API_PASS:-secret}"
ports:
- "8000:8000"
logging:
driver: "json-file"
options:
max-size: "1m"
max-file: "2"
\ No newline at end of file
This diff is collapsed.
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.2 18.2"><defs><style>.cls-1{fill:#ecd399;}</style></defs><title>Ikoner</title><circle cx="9.1" cy="9.1" r="9.1"/><path class="cls-1" d="M12.06,13.7a.508.508,0,0,1-.359-.149l-2.6-2.6-2.6,2.6a.507.507,0,0,1-.866-.359V5.318a.507.507,0,0,1,.507-.507h5.92a.507.507,0,0,1,.507.507v7.874a.508.508,0,0,1-.507.508ZM9.1,9.726a.506.506,0,0,1,.359.148l2.094,2.094V5.825H6.647v6.143L8.741,9.874A.506.506,0,0,1,9.1,9.726Z"/></svg>
\ No newline at end of file
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.2 18.2"><defs><style>.cls-1{opacity:0.7;}.cls-2{fill:#ecd399;}</style></defs><title>Ikoner</title><circle class="cls-1" cx="9.1" cy="9.1" r="7.017"/><path class="cls-2" d="M11.382,12.647a.389.389,0,0,1-.276-.115L9.1,10.526,7.094,12.532a.39.39,0,0,1-.667-.276V6.184a.391.391,0,0,1,.391-.391h4.564a.391.391,0,0,1,.391.391v6.072a.391.391,0,0,1-.391.391ZM9.1,9.582a.39.39,0,0,1,.276.115l1.615,1.615V6.575H7.209v4.737L8.824,9.7A.39.39,0,0,1,9.1,9.582Z"/></svg>
\ No newline at end of file
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.2 18.2"><defs><style>.cls-1{fill:#ecd399;}</style></defs><title>Ikoner</title><circle cx="9.1" cy="9.1" r="9.1"/><path class="cls-1" d="M12.06,13.7a.508.508,0,0,1-.359-.149l-2.6-2.6-2.6,2.6a.507.507,0,0,1-.866-.359V5.318a.507.507,0,0,1,.507-.507h5.92a.507.507,0,0,1,.507.507v7.874a.508.508,0,0,1-.507.508Z"/></svg>
\ No newline at end of file
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18.2 18.2"><defs><style>.cls-1{opacity:0.7;}.cls-2{fill:#ecd399;}</style></defs><title>Ikoner</title><circle class="cls-1" cx="9.1" cy="9.1" r="7.017"/><path class="cls-2" d="M11.382,12.647a.389.389,0,0,1-.276-.115L9.1,10.526,7.094,12.532a.39.39,0,0,1-.667-.276V6.184a.391.391,0,0,1,.391-.391h4.564a.391.391,0,0,1,.391.391v6.072a.391.391,0,0,1-.391.391Z"/></svg>
\ No newline at end of file
# must be unique in a given SonarQube instance
sonar.projectKey=no.deichman.timber:timber
sonar.projectName=Timber
sonar.projectVersion=0.0.1
sonar.sources=.
# Encoding of the source code. Default is default system encoding
#sonar.sourceEncoding=UTF-8
......@@ -4,45 +4,7 @@ module.exports = (app) => {
return (url, opts) => {
opts = opts || {}
opts.headers = opts.headers || {}
opts.headers[ "Cookie" ] = app.settings.kohaSession
opts.headers[ "credentials" ] = "include"
return isofetch(url, opts)
.then(res => {
if (res.status === 403 || res.status === 401) {
const dup = res.clone() // duplicate body for logging
return dup.json().then(json => {
if (res.status >= 400) {
console.log(`Call to ${url} with options ${JSON.stringify(opts)}:`)
console.log(`${res.status}: ${JSON.stringify(json)}`)
}
if (json.error === "Authentication required." || json.error === "Authentication failure.") {
// Unauthorized; we try to renew session and then retry request.
return isofetch("http://koha:8081/api/v1/auth/session", {
method: "POST",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: `userid=${encodeURIComponent(process.env.KOHA_API_USER)}&password=${encodeURIComponent(process.env.KOHA_API_PASS)}`
})
.then(res => {
if (res.headers && res.headers._headers && res.headers._headers[ "set-cookie" ] && res.headers._headers[ "set-cookie" ][ 0 ]) {
app.set("kohaSession", res.headers._headers[ "set-cookie" ][ 0 ])
opts.headers[ "Cookie" ] = app.settings.kohaSession
// We renewed the session; retry original HTTP call (once)
return isofetch(url, opts)
} else {
console.log(res)
throw new Error("Cannot obtain Koha API session")
}
})
} else {
return res
}
})
} else {
return res
}
})
}
}
......@@ -70,7 +70,7 @@
},
"hasNumberOfPerformers": {
"@id": "deichman:hasNumberOfPerformers",
"@type": "xsd:integer"
"@type": "xsd:nonNegativeInteger"
},
"hasInstrumentation": {
"@id": "deichman:hasInstrumentation",
......@@ -379,6 +379,11 @@
}
},
"type": "deichman:WorkInFocus",
"hasInstrumentation": {
"hasInstrument": {
"@embed": "@always"
}
},
"subjects": {
"@type": [
"Work",
......
......@@ -4,73 +4,66 @@ const bcrypt = require("bcrypt-nodejs")
module.exports = (app) => {
const fetch = require("../fetch")(app)
app.post("/api/v1/login", jsonParser, (request, response) => {
app.post("/api/v1/login", jsonParser, async (request, response) => {
if (!request.body.username || !request.body.password) {
return response.sendStatus(403)
}
let borrowerNumber
let homeBranch
let category
captchaHandler(request.body.captcha)
.then(res => loginHandler(request.body.username))
.then(res => {
if (res.status === 200) {
return res.json()
} else {
return Promise.reject({ message: "User not found", status: 403 })
}
})
.then(json => {
// only unique user should be able to create user session
if (json.length !== 1) {
return Promise.reject({ message: "User not unique", status: 403 })
} else {
borrowerNumber = json[ 0 ].borrowernumber
homeBranch = json[ 0 ].branchcode
category = json[ 0 ].categorycode
return fetch("http://koha:8081/api/v1/auth/session", {
method: "POST",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: `userid=${encodeURIComponent(json[ 0 ].userid)}&password=${encodeURIComponent(request.body.password)}`
})
}
try {
await captchaHandler(request.body.captcha)
const res = await fetch("http://koha:8081/api/v1/auth/session", {
method: "POST",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: `userid=${encodeURIComponent(request.body.username)}&password=${encodeURIComponent(request.body.password)}`
})
.then(res => {
if (res.status === 201) {
request.session.borrowerNumber = borrowerNumber
request.session.homeBranch = homeBranch
request.session.kohaSession = res.headers._headers[ "set-cookie" ][ 0 ]
request.session.passwordHash = bcrypt.hashSync(request.body.password)
request.session.category = category
return res.json()
} else if (res.status === 429 && process.env.RECAPTCHA_SECRET) {
return Promise.reject({ message: "To many failed attempts", status: 429 })
} else {
return Promise.reject({ message: "Could not create session", status: 403 })
if (res.status === 201) {
const cookies = res.headers._headers[ "set-cookie" ]
const kohaCookie = cookies.find(el => { return /^koha.session=\d+.\d+.\d+/.test(el) })
request.session.kohaCookie = kohaCookie
const sessid = kohaCookie.match(/\d+.\d+.\d+/)
if (sessid) {
request.session.kohaSession = sessid[0]
}
request.session.passwordHash = bcrypt.hashSync(request.body.password)
} else if (res.status === 429 && process.env.RECAPTCHA_SECRET) {
response.sendStatus(429)
return
} else {
response.sendStatus(403)
return
}
const user = await res.json()
request.session.borrowerNumber = user.borrowernumber
request.session.homeBranch = user.branchcode
request.session.category = user.categorycode
const borrowerName = request.session.borrowerName = `${user.firstname ? user.firstname : ""}${user.firstname && user.surname ? " " : ""}${user.surname ? user.surname : ""}`
request.session.firstName = `${user.firstname ? user.firstname : ""}`
response.send({
isLoggedIn: true,
borrowerNumber: request.session.borrowerNumber,
borrowerName: borrowerName,
firstName: request.session.firstName,
homeBranch: request.session.homeBranch,
category: request.session.category
})
.then(json => {
const borrowerName = request.session.borrowerName = `${json.firstname ? json.firstname : ""}${json.firstname && json.surname ? " " : ""}${json.surname ? json.surname : ""}`
response.send({
isLoggedIn: true,
borrowerNumber: request.session.borrowerNumber,
borrowerName: borrowerName,
homeBranch: request.session.homeBranch,
category: request.session.category
})
})
.catch(error => {
console.log(error.message)
response.sendStatus(error.status)
})
} catch (error) {
console.log("LOGIN ERROR : %j", error)
response.sendStatus(500)
}
})
app.post("/api/v1/logout", (request, response) => {
app.post("/api/v1/logout", async (request, response) => {
await fetch("http://koha:8081/api/v1/auth/session", {
method: "DELETE",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify({ sessionid: request.session.kohaSession })
})
request.session.destroy((error) => error ? response.sendStatus(500) : response.sendStatus(200))
})
......@@ -79,19 +72,12 @@ module.exports = (app) => {
isLoggedIn: request.session.borrowerNumber !== undefined,
borrowerNumber: request.session.borrowerNumber,
borrowerName: request.session.borrowerName,
firstName: request.session.firstName,
homeBranch: request.session.homeBranch,
category: request.session.category
})
})
function loginHandler (username) {
if (/^[^@ ]+@[^@ ]+$/i.test(username)) {
return fetch(`http://koha:8081/api/v1/patrons?email=${username}`)
} else {
return fetch(`http://koha:8081/api/v1/patrons?userid=${username}`)
}
}
function captchaHandler (captcha) {
if (!captcha || !process.env.RECAPTCHA_SECRET) {
return Promise.resolve({})
......
......@@ -7,7 +7,10 @@ module.exports = (app) => {
const fetch = require("../fetch")(app)
app.put("/api/v1/checkouts", jsonParser, (request, response) => {
fetch(`http://koha:8081/api/v1/checkouts/${request.body.checkoutId}`, {
method: "PUT"
method: "PUT",
headers: {
Cookie: `koha.session=${request.session.kohaSession}`
}
}).then(res => {
if (res.status === 200 || res.status === 403) {
return res.json()
......@@ -76,7 +79,8 @@ module.exports = (app) => {
method: "POST",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cookie": `koha.session=${request.session.kohaSession}`
},
body: `purre_id=${encodeURIComponent(request.body.fineId)}&nets_id=${encodeURIComponent(transactionId)}`
})
......@@ -124,14 +128,22 @@ module.exports = (app) => {
const transactionId = jsonResponse.ProcessResponse.TransactionId.join("")
// Get all loans from koha
const loansRes = await fetch(`http://koha:8081/api/v1/patrons/${request.session.borrowerNumber}/loansandreservations`)
const loansRes = await fetch(`http://koha:8081/api/v1/patrons/${request.session.borrowerNumber}/loansandreservations`, {
method: "GET",
headers: {
Cookie: `koha.session=${request.session.kohaSession}`
}
})
const loans = await loansRes.json()
// Extend all loans with isPurresak
const successfulExtends = []
for (const loan of loans.loans) {
if (loan.isPurresak) {
const extendRes = await fetch(`http://koha:8081/api/v1/checkouts/${loan.id}?override_days=3`, {
method: "PUT"
method: "PUT",
headers: {
Cookie: `koha.session=${request.session.kohaSession}`
}
})
if (extendRes.status === 200) {
const extend = await extendRes.json()
......@@ -146,7 +158,8 @@ module.exports = (app) => {
method: "PUT",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cookie": `koha.session=${request.session.kohaSession}`
},
body: `nets_id=${encodeURIComponent(transactionId)}`
})
......@@ -186,7 +199,8 @@ module.exports = (app) => {
method: "PUT",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cookie": `koha.session=${request.session.kohaSession}`
},
body: `nets_id=${encodeURIComponent(transactionId)}&email=${encodeURIComponent(email)}&authorizationId=${encodeURIComponent(authorizationId)}&successfulExtends=${encodeURIComponent(successfulExtends)}`
})
......
......@@ -6,6 +6,9 @@ module.exports = (app) => {
app.post("/api/v1/holds", jsonParser, (request, response) => {
fetch("http://koha:8081/api/v1/holds", {
method: "POST",
headers: {
Cookie: `koha.session=${request.session.kohaSession}`
},
body: JSON.stringify({
borrowernumber: Number(request.session.borrowerNumber),
biblionumber: Number(request.body.recordId),
......@@ -30,7 +33,10 @@ module.exports = (app) => {
app.delete("/api/v1/holds", jsonParser, (request, response) => {
fetch(`http://koha:8081/api/v1/holds/${request.body.reserveId}`, {
method: "DELETE"
method: "DELETE",
headers: {
Cookie: `koha.session=${request.session.kohaSession}`
}
}).then(res => {
if (res.status === 200) {
response.sendStatus(200)
......@@ -58,6 +64,9 @@ module.exports = (app) => {
fetch(`http://koha:8081/api/v1/holds/${request.body.reserveId}`, {
method: "PATCH",
headers: {
Cookie: `koha.session=${request.session.kohaSession}`
},
body: JSON.stringify(reserveModifications)
}).then(res => {
if (res.status === 200) {
......@@ -80,7 +89,8 @@ module.exports = (app) => {
method: "POST",
headers: {
"Accept": "application/json, application/xml, text/plain, text/html, *.*",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"Cookie": `koha.session=${request.session.kohaSession}`
},
body: `remote_library=${encodeURIComponent(request.session.borrowerNumber)}` +
`&biblionumber=${encodeURIComponent(request.body.recordId)}` +
......
......@@ -6,6 +6,7 @@ module.exports = (app) => {
require("./holds")(app)
require("./profile")(app)
require("./libraries")(app)
require("./libraryService")(app)
require("./registration")(app)
require("./resources")(app)
require("./search")(app)
......
const bodyParser = require("body-parser")
const jsonParser = bodyParser.json()
const debug = require("debug")("libraryService")
const EXPIRATION_WINDOW_IN_SECONDS = 300
const tokenConfig = {
scope: []
}
const credentials = {
client: {
id: process.env.CLIENTID,
secret: process.env.CLIENTSECRET
},
auth: {
tokenHost: process.env.AUTH_SERVER_URL
},
options: {
authorizationMethod: "header"
}
}
const oauth2 = require("simple-oauth2").create(credentials)
let accessToken
module.exports = (app) => {
const fetch = require("../fetch")(app)
app.get("/api/v1/libraryService/branch/:id", jsonParser, async (request, response) => {
try {
const accessToken = await checkToken()
if (accessToken == null) {
throw Error("Missing Access Token")
}
const id = request.params.id
const eulerRes = await fetch(`http://euler:8080/api/library-services/branches/${id}`, {
method: "GET",
headers: { Authorization: `Bearer ${accessToken.token.access_token}` }
})
if (eulerRes.status !== 200) {
throw Error(eulerRes.statusText)
}
const json = await eulerRes.json()
response.status(200).send(json)
} catch (error) {
console.error("Call to euler failed", error.message)
response.sendStatus(500)
}
})
}
async function checkToken () {
// Get the access token object for the client
if (accessToken == null) {
try {
const result = await oauth2.clientCredentials.getToken(tokenConfig)
accessToken = oauth2.accessToken.create(result)
debug("New Access Token")
} catch (error) {
console.warn("Access Token error", error.message)
}
} else {
const { token } = accessToken
const expirationTimeInSeconds = token.expires_at.getTime() / 1000
const expirationWindowStart = expirationTimeInSeconds - EXPIRATION_WINDOW_IN_SECONDS
// If the start of the window has passed, refresh the token
const nowInSeconds = (new Date()).getTime() / 1000
const shouldRefresh = nowInSeconds >= expirationWindowStart