feat: optimise images with next/image (#54)

* chore: Accessability fix in css. Resolves #29

* feat: upgrade to next@10 to be able to use next/image

* converted from huge images to optimized images with next/image

* chore: 🤖 Removed unused API project and fixed pr test action

Co-authored-by: Johan Öbrink <johan.obrink@gmail.com>
This commit is contained in:
irony 2021-02-08 21:00:23 +01:00 committed by GitHub
parent 823818bc92
commit fbbf6feaf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 691 additions and 9221 deletions

View File

@ -5,6 +5,7 @@
"lerna": "^3.22.1"
},
"scripts": {
"test": "lerna run test --stream"
"bootstrap": "npx lerna bootstrap",
"test": "npx lerna run test --stream"
}
}

View File

@ -1 +0,0 @@
node_modules

View File

@ -1 +0,0 @@
v14.15.0

View File

@ -1,8 +0,0 @@
FROM node
WORKDIR /app
ADD package.json .
RUN npm install --production
ADD . /app/
ENV PORT=9000
EXPOSE 9000
CMD node index.js

View File

@ -1,22 +0,0 @@
# $kolplattformen API - DEPRECATED
## DEPRECATED
We are not using this API anymore, we keep it here for reference and some day this might be the starting point for a new consistant and documented official API based on OpenAPI maybe?
## Develop
### Run
```npm run dev```
Log all requests through to Skolplattformen:
```LOG_REQUESTS=true npm run dev```
### Tests
```npm test```
Continuously:
```npm test -- --watchAll```

View File

@ -1,122 +0,0 @@
const OpenAPIBackend = require('openapi-backend').default
const express = require('express')
const backend = require('./lib/backend')
const { deconstruct, verifyToken, createToken } = require('./lib/credentials')
const app = express()
app.use(express.json())
app.use('/spec', express.static('spec'))
app.use('/', express.static('public'))
// define api
const api = new OpenAPIBackend({ definition: './spec/skolplattformen-1.0.0.yaml' })
// TODO: check jwt secret if prod
// register default handlers
api.register({
notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
unauthorizedHandler: async (c, req, res) => res.status(401).json({ err: 'unauthorized' }),
})
// register security handler for jwt auth
api.registerSecurityHandler('bearerAuth', (c, req, res) => {
try {
const { cookie } = verifyToken(c)
return cookie
} catch (err) {
const {message, stack} = err
res.status(500).json({message, stack})
}
})
// register operation handlers
api.register({
login: async (c, req, res) => {
try {
console.log('login initiated')
const { socialSecurityNumber } = deconstruct(c)
const token = await backend.login(socialSecurityNumber)
return res.status(200).json(token)
} catch (err) {
return res.status(err.status || 500).json({ message: err.message, stack: err.stack })
}
},
waitForToken: async (c, req, res) => {
console.log('wait for token')
const { order } = deconstruct(c)
try {
const cookie = await backend.waitForToken({order})
const token = createToken(cookie)
return res.status(200).json({token})
} catch (err) {
return res.status(500).json({err})
}
},
getChildren: async (c, req, res) => {
console.log('get children')
const { cookie } = deconstruct(c)
try {
const children = await backend.getChildren(cookie)
return res.status(200).json(children)
} catch (err) {
return res.status(500).json(err)
}
},
getChildById: async (c, req, res) => {
const { cookie, childId } = deconstruct(c)
try {
const child = await backend.getChildById(childId, cookie)
return res.status(200).json(child)
} catch (err) {
return res.status(500).json(err)
}
},
getNews: async (c, req, res) => {
const { cookie, childId } = deconstruct(c)
const news = await backend.getNews(childId, cookie)
return res.status(200).json(news)
},
getCalendar: async (c, req, res) => {
const { cookie, childId } = deconstruct(c)
const calendar = await backend.getCalendar(childId, cookie)
return res.status(200).json(calendar)
},
getNotifications: async (c, req, res) => {
const { cookie, childSdsId } = deconstruct(c)
const notifications = await backend.getNotifications(childSdsId, cookie)
return res.status(200).json(notifications)
},
getMenu: async (c, req, res) => {
const { cookie, childId } = deconstruct(c)
const menu = await backend.getMenu(childId, cookie)
return res.status(200).json(menu)
},
getSchedule: async (c, req, res) => {
const { cookie, childSdsId } = deconstruct(c)
const schedule = await backend.getSchedule(childSdsId, cookie)
return res.status(200).json(schedule)
},
getClassmates: async (c, req, res) => {
const { cookie, childSdsId } = deconstruct(c)
const classmates = await backend.getClassmates(childSdsId, cookie)
return res.status(200).json(classmates)
},
download: async (c, req, res) => {
const { cookie, url } = deconstruct(c)
const stream = await backend.download(url, cookie)
stream.body.pipe(res.body)
}
})
api.init()
// use as express middleware
app.use((req, res) => api.handleRequest(req, req, res))
// start server
const server = app.listen(process.env.PORT || 9000, () => console.info(`api listening at http://localhost:${process.env.PORT || 9000}`))
server.setTimeout(process.env.REQUEST_TIMEOUT || 200 * 1000)

View File

@ -1,223 +0,0 @@
const moment = require('moment')
const h2m = require('h2m')
const { htmlDecode } = require('js-htmlencode')
const urls = require('./urls')
const { fetchJson, fetchText, fetchRaw } = require('./fetch')
const pause = ms => new Promise((resolve) => setTimeout(resolve, ms))
const download = (url, cookie) => fetchRaw(url, cookie)
const login = async (socialSecurityNumber) => {
const url = urls.login(socialSecurityNumber)
const token = await fetchJson(url)
console.log('login started')
return token
}
const waitForToken = async ({ order }, tries = 60) => {
if (!tries) return Promise.reject('Timeout')
const status = await fetchText(urls.checkStatus(order))
if (status === 'OK') {
const result = await fetchRaw(urls.loginTarget)
return result.headers.get('set-cookie')
} else {
await pause(1000)
console.log('retry', tries)
return await waitForToken({ order }, tries--)
}
}
const getChildren = async (cookie) => {
const children = await fetchJson(urls.children, cookie)
return children
.map(({ name, id, sdsId, status, schoolId }) =>
({ name, id, sdsId, status, schoolId }))
}
const getNews = async (childId, cookie) => {
const news = await fetchJson(urls.news(childId), cookie)
return news
.newsItems
.map(({
body,
preamble: intro,
header,
bannerImageUrl: imageUrl,
pubDateSE: published,
modDateSE: modified
}) => ({
header,
intro,
body: htmlDecode(h2m(body)),
modified,
published,
imageUrl: urls.image(imageUrl)
}))
}
const getCalendar = async (childId, cookie) => {
const url = urls.calendar(childId)
const calendar = await fetchJson(url, cookie)
return calendar
.map(({
title,
id,
description,
location,
longEventDateTime: startDate,
longEndDateTime: endDate,
allDayEvent: allDay
}) => ({
title,
id,
description,
location,
startDate: moment(startDate, 'YYYY-MM-DD hh:mm').toISOString(),
endDate: moment(endDate, 'YYYY-MM-DD hh:mm').toISOString(),
allDay
}))
}
const getNotifications = async (childId, cookie) => {
const url = urls.notifications(childId)
const notifications = await fetchJson(url, cookie)
return notifications
.map(({
notificationMessage: {
messages: {
message: {
messageid: id,
messagetext: message,
messagetime: dateCreated,
linkbackurl: url,
sender,
category,
messagetype: {
type: messageType
}
}
} = {}
}
}) => ({
id,
sender,
dateCreated: moment(dateCreated).toISOString(),
message,
url,
category,
messageType
}))
}
const getMenu = async (childId, cookie) => {
const url = urls.menu(childId)
const menu = await fetchJson(url, cookie)
return menu
}
const getSchedule = async (childId, cookie) => {
const from = moment().format('YYYY-MM-DD')
const to = moment().add(7, 'days').format('YYYY-MM-DD')
const url = urls.schedule(childId, from, to)
const schedule = await fetchJson(url, cookie)
return schedule
.map(({
title,
id,
description,
location,
longEventDateTime: startDate,
longEndDateTime: endDate,
allDayEvent: allDay,
mentor
}) => ({
title,
id,
description,
location,
startDate: moment(startDate, 'YYYY-MM-DD hh:mm').toISOString(),
endDate: moment(endDate, 'YYYY-MM-DD hh:mm').toISOString(),
allDay,
mentor
}))
}
const getClassmates = async (childId, cookie) => {
const url = urls.classmates(childId)
const classmates = await fetchJson(url, cookie)
return classmates
.map(({
sisId,
firstname,
lastname,
location,
guardians = [],
className
}) => ({
sisId,
firstname,
lastname,
location,
guardians: guardians.map(({
emailhome: email,
firstname,
lastname,
telmobile: mobile,
address
}) => ({
email,
firstname,
lastname,
mobile,
address
})),
className
}))
}
const getChildById = async (childId, cookie) => {
const children = await getChildren()
const child = children.find(c => c.id == childId)
const [
news,
calendar,
notifications,
menu,
schedule
] = [
await getNews(childId, cookie),
await getCalendar(childId, cookie),
await getNotifications(child.sdsId, cookie),
await getMenu(child.id, cookie),
await getSchedule(child.id, cookie)
]
return {
child,
news,
calendar,
notifications,
menu,
schedule
}
}
module.exports = {
login,
waitForToken,
getChildren,
getChildById,
getNews,
getCalendar,
getNotifications,
getMenu,
getSchedule,
getClassmates,
download
}

View File

@ -1,59 +0,0 @@
const jwt = require('jsonwebtoken')
const moment = require('moment')
const { encrypt, decrypt } = require('./crypto')
const deconstruct = (c) => {
const result = {}
if (c.security && c.security.bearerAuth) {
result.cookie = decrypt(c.security.bearerAuth)
}
if (c.request.headers) {
if (c.request.headers.authorization) {
result.authorization = c.request.headers.authorization.replace('Bearer ', '')
}
}
if (c.request.params) {
result.order = c.request.params.order
result.childId = c.request.params.childId
result.childSdsId = c.request.params.childSdsId
}
if (c.request.query) {
result.socialSecurityNumber = c.request.query.socialSecurityNumber
result.url = c.request.query.url
}
return result
}
const verifyToken = (c) => {
try {
const { authorization } = deconstruct(c)
if (!authorization) {
throw new Error('Missing authorization header')
}
const decoded = jwt.verify(authorization, process.env.JWT_SECRET || 'secret')
return decoded
} catch (err) {
throw err
}
}
const createToken = (cookie) => {
const objectified = cookie
.split('; ')
.map((slug) => slug.split('='))
.reduce((obj, [key, val]) => ({...obj, [key]: val}), {})
const options = {}
if (objectified.expires) {
options.expiresIn = moment(new Date(objectified.expires)).unix() - moment().unix()
}
return jwt.sign({
cookie: encrypt(cookie)
}, process.env.JWT_SECRET || 'secret', options)
}
module.exports = {
createToken,
deconstruct,
verifyToken
}

View File

@ -1,99 +0,0 @@
const jwt = require('jsonwebtoken')
const moment = require('moment')
const { createToken, deconstruct, verifyToken } = require('./credentials')
const { encrypt, decrypt } = require('./crypto')
describe('credentials', () => {
describe('createToken', () => {
let cookie, expires
beforeEach(() => {
process.env.JWT_SECRET = 'correct'
expires = moment().add(1, 'd')
const weirdFormatTimeString = expires.utc().format('ddd, DD-MMM-YYYY HH:mm:ss') + ' GMT'
cookie = `SMSESSION=foobar; path=/; HttpOnly; SameSite=Lax, StockholmEServiceLanguage=1053,Svenska,Language,sv; expires=${weirdFormatTimeString}; path=/`
})
it('creates a jwt', () => {
const token = createToken(cookie)
expect(token.split('.')).toHaveLength(3)
})
it('stores cookie', () => {
const token = createToken(cookie)
const decoded = jwt.verify(token, process.env.JWT_SECRET)
expect(decrypt(decoded.cookie)).toEqual(cookie)
})
it('sets expiry', () => {
const token = createToken(cookie)
const decoded = jwt.verify(token, process.env.JWT_SECRET)
expect(decoded.exp).toEqual(expires.unix())
})
})
describe('verifyToken', () => {
let token, c, cookie
beforeEach(() => {
process.env.JWT_SECRET = 'correct'
cookie = 'SMSESSION=foobar'
token = jwt.sign({ cookie }, process.env.JWT_SECRET)
c = {
request: {
headers: {
authorization: `Bearer ${token}`
}
}
}
})
it('throws if header is missing', () => {
c.request.headers = {}
expect(() => verifyToken(c)).toThrow('Missing authorization header')
})
it('throws if signature is wrong', () => {
token = jwt.sign({ cookie }, 'wrong')
c.request.headers.authorization = `Bearer ${token}`
expect(() => verifyToken(c)).toThrow('invalid signature')
})
it('returns a jwt on successs', () => {
const decoded = verifyToken(c)
expect(decoded.cookie).toEqual(cookie)
})
})
describe('deconstruct', () => {
let c
beforeEach(() => {
c = {
request: {
headers: {
authorization: 'Bearer authorization'
},
params: {
order: 'abc-123',
childId: 'childId',
childSdsId: 'childSdsId'
},
query: {
socialSecurityNumber: '200001019999',
url: 'https://google.com'
},
},
security: {
bearerAuth: encrypt('bearerAuth')
}
}
})
it('returns all expected values', () => {
expect(deconstruct(c)).toEqual({
authorization: 'authorization',
order: 'abc-123',
childId: 'childId',
childSdsId: 'childSdsId',
socialSecurityNumber: '200001019999',
cookie: 'bearerAuth',
url: 'https://google.com'
})
})
})
})

View File

@ -1,51 +0,0 @@
const {
createCipheriv,
createDecipheriv,
createHash,
randomBytes
} = require('crypto')
const encryptionKey = process.env.JWT_ENCRYPTION_KEY || 'encrypt'
const ALGORITHM = 'aes-256-gcm'
const toBase64Url = (b64) => b64.replace(/\=/g, '').replace(/\+/g, '-').replace(/\\/g, '_')
const fromBase64Url = (b64u) => b64u.replace(/\-/g, '+').replace(/_/g, '\\')
const cipherKey = () => createHash('sha256').update(encryptionKey).digest()
const encrypt = (text) => {
const iv = randomBytes(16)
const key = cipherKey()
const cipherBuffer = Buffer.from(text, 'utf8')
const cipherIv = createCipheriv(ALGORITHM, key, iv)
const [buf1, buf2] = [cipherIv.update(cipherBuffer), cipherIv.final()]
const auth = cipherIv.getAuthTag()
const base64 = Buffer
.concat(
[iv, auth, buf1, buf2],
32 + buf1.length + buf2.length
)
.toString('base64')
return toBase64Url(base64)
}
const decrypt = (encrypted) => {
const buffer = Buffer.from(fromBase64Url(encrypted), 'base64')
const iv = buffer.slice(0, 16)
const auth = buffer.slice(16, 32)
const cipher = buffer.slice(32)
const key = cipherKey()
const decipherIv = createDecipheriv(ALGORITHM, key, iv)
decipherIv.setAuthTag(auth)
const [buf1, buf2] = [decipherIv.update(cipher), decipherIv.final()]
const deciphered = Buffer.concat([buf1, buf2], buf1.length, buf2.length)
return deciphered.toString('utf8')
}
module.exports = {
encrypt,
decrypt
}

View File

@ -1,32 +0,0 @@
const { encrypt, decrypt } = require('./crypto')
describe('crypto', () => {
it('encrypts', () => {
const encrypted = encrypt('message')
expect(typeof encrypted).toBe('string')
})
it('decrypts', () => {
const encrypted = encrypt('message')
const decrypted = decrypt(encrypted)
expect(decrypted).toEqual('message')
})
it('decrypts long texts', () => {
const msg = `
lorem ipsum dolor sit amet, consectetur adipiscing elit
ut aliquam, purus sit amet luctus venenatis, lectus magna
fringilla urna, porttitor rhoncus dolor purus non enim
praesent elementum facilisis leo, vel fringilla est
ullamcorper eget nulla facilisi etiam dignissim diam quis
enim lobortis scelerisque fermentum dui faucibus in ornare
quam viverra orci sagittis eu volutpat odio facilisis
mauris sit amet massa vitae tortor condimentum lacinia
quis vel eros donec ac odio tempor orci dapibus ultrices
in iaculis nunc sed augue lacus, viverra vitae congue eu,
consequat ac felis donec et odio pellentesque diam volutpat
commodo sed egestas egestas fringilla phasellus faucibus
`
const encrypted = encrypt(msg)
const decrypted = decrypt(encrypted)
expect(decrypted).toEqual(msg)
})
})

View File

@ -1,125 +0,0 @@
const nodeFetch = require('node-fetch')
const fetch = require('fetch-cookie/node-fetch')(nodeFetch)
const createError = require('http-errors')
const camel = require('camelcase-keys')
const { logRequest } = require('./logger')
const logging = (f) => async (url, cookie) => {
const log = {}
try {
const result = await f(url, cookie, log)
return result
} finally {
if (process.env.LOG_REQUESTS) {
console.error(log)
logRequest(log)
}
}
}
const errorExists = (errors, {message, status}) => {
return !!errors.find((e) => (
e.message === message &&
e.status === status
))
}
const logError = (log, error) => {
if (error) return
const {message, status, stack} = error
if (!log.errors) {
log.errors = [{message, status, stack}]
} else if (!errorExists(error)) {
log.errors.push({message, status, stack})
}
}
const fetchRaw = async (url, cookie, log = {}) => {
const options = {
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.87 Safari/537.36'
}
if (cookie) {
options.headers = {cookie}
}
log.request = {
url,
...options
}
const response = await fetch(url, options)
log.response = {
ok: response.ok,
status: response.status,
statusText: response.statusText,
headers: {
'content-type': response.headers.get('content-type')
}
}
if (!response.ok) {
const error = createError(response.status, response.statusText)
logError(log, error)
throw error
}
return response
}
const fetchText = async (url, cookie, log = {}) => {
try {
const response = await fetchRaw(url, cookie, log)
const text = await response.text()
log.response.body = text
return text
} catch (error) {
if (!log.response.body) {
try {
log.response.body = await parse(response)
} catch (_) {}
}
logError(log, error)
throw error
}
}
const fetchJson = async (url, cookie, log = {}) => {
try {
const response = await fetchRaw(url, cookie, log)
if (response.headers.get('content-type').split(';')[0] !== 'application/json') {
log.response.body = await response.text()
throw new Error('Expected JSON')
}
const json = await response.json()
log.response.body = json
const camelized = camel(json, { deep: true })
if (camelized.error) {
throw camelized.error
}
return camelized.data || camelized
} catch (error) {
if (!log.response.body) {
try {
log.response.body = await parse(response)
} catch (_) {}
}
logError(log, error)
throw error
}
}
const parse = async (response) => {
const contentType = response.headers.get('content-type').split(';')[0]
switch (contentType) {
case 'application/json':
const json = await response.json()
return camel(json, { deep: true })
case 'text/html':
return response.text()
default:
return
}
}
module.exports = {
fetchRaw: logging(fetchRaw),
fetchJson: logging(fetchJson),
fetchText: logging(fetchText)
}

View File

@ -1,34 +0,0 @@
const { writeFile } = require('fs/promises')
const { join } = require('path')
const mkdirp = require('mkdirp')
const wait = (ms) => new Promise(r => setTimeout(r, ms))
const logRequest = async (log) => {
try {
const [,,host,...slugs] = log.request.url.split('/')
const [sub] = host.split('.')
const last = slugs.pop()
const filename = []
filename.push((last.indexOf('?') > 0
? last.substring(0, last.indexOf('?'))
: last))
if (log.errors) {
filename.push('_error')
}
filename.push('.json')
const dir = join(process.cwd(), 'requests', sub, ...slugs)
await mkdirp(dir)
await wait(100)
await writeFile(
join(dir, filename.join('')),
JSON.stringify(log, null, 2)
)
} catch (err) {
console.error(err)
}
}
module.exports = {
logRequest
}

View File

@ -1,18 +0,0 @@
const baseUrl = 'https://etjanst.stockholm.se'
const urls = {
login: socialSecurityNumber => `https://login003.stockholm.se/NECSadcmbid/authenticate/NECSadcmbid?TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt&initialize=bankid&personalNumber=${socialSecurityNumber}&_=${Date.now()}`,
checkStatus: order => `https://login003.stockholm.se/NECSadcmbid/authenticate/NECSadcmbid?TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt&verifyorder=${order}&_=${Date.now()}`,
//loginTarget: `https://login001.stockholm.se/NECSadc/mbid/b64startpage.jsp?startpage=aHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvaGVt`,
loginTarget: 'https://login003.stockholm.se/NECSadcmbid/authenticate/SiteMinderAuthADC?TYPE=33554433&REALMOID=06-42f40edd-0c5b-4dbc-b714-1be1e907f2de&GUID=&SMAUTHREASON=0&METHOD=GET&SMAGENTNAME=IfNE0iMOtzq2TcxFADHylR6rkmFtwzoxRKh5nRMO9NBqIxHrc38jFyt56FASdxk1&TARGET=-SM-HTTPS%3a%2f%2flogin001%2estockholm%2ese%2fNECSadc%2fmbid%2fb64startpage%2ejsp%3fstartpage%3daHR0cHM6Ly9ldGphbnN0LnN0b2NraG9sbS5zZS92YXJkbmFkc2hhdmFyZS9pbmxvZ2dhZDIvR2V0Q2hpbGRyZW4%3d',
children: `${baseUrl}/vardnadshavare/inloggad2/GetChildren`,
classmates: childId => `${baseUrl}/vardnadshavare/inloggad2/contacts/GetStudentsByClass?studentId=${childId}`,
calendar: childId => `${baseUrl}/vardnadshavare/inloggad2/Calender/GetSchoolCalender?childId=${childId}&rowLimit=50`,
user: `${baseUrl}/vardnadshavare/base/getuserdata`,
news: childId => `${baseUrl}/vardnadshavare/inloggad2/News/GetNewsOverview?childId=${childId}`,
image: url => `${baseUrl}/vardnadshavare/inloggad2/NewsBanner?url=${url}`,
notifications: childId => `${baseUrl}/vardnadshavare/inloggad2/Overview/GetNotification?childId=${childId}`,
menu: childId => `${baseUrl}/vardnadshavare/inloggad2/Matsedel/GetMatsedelRSS?childId=${childId}`,
schedule: (childId, fromDate, endDate) => `${baseUrl}/vardnadshavare/inloggad2/Calender/GetSchema?childId=${childId}&startDate=${fromDate}&endDate=${endDate}`
}
module.exports = urls

View File

@ -1,4 +0,0 @@
{
"verbose": false,
"ignore": ["*.test.js", "fixtures/*", "requests/*"]
}

View File

@ -1,39 +0,0 @@
{
"name": "skolplattformen-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"camelcase-keys": "^6.2.2",
"cookie": "^0.4.1",
"decode-html": "^2.0.0",
"express": "^4.17.1",
"fetch-cookie": "^0.11.0",
"h2m": "^0.7.0",
"http-errors": "^1.8.0",
"js-htmlencode": "^0.3.0",
"jsonwebtoken": "^8.5.1",
"mkdirp": "^1.0.4",
"moment": "^2.29.1",
"node-fetch": "^2.6.1",
"openapi-backend": "^3.6.3"
},
"devDependencies": {
"@types/jest": "^26.0.19",
"@types/jsonwebtoken": "^8.5.0",
"@types/node-fetch": "^2.5.7",
"eslint": "^7.14.0",
"eslint-config-standard": "^16.0.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"jest": "^26.6.3",
"nodemon": "^2.0.6"
},
"scripts": {
"dev": "nodemon",
"test": "jest"
},
"author": "",
"license": "ISC"
}

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>ReDoc</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url='/spec/skolplattformen-1.0.0.yaml'></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -1,43 +0,0 @@
/*
const backend = require('./backend')
const run = async (socialSecurityNumber) => {
const OK = await backend.login(socialSecurityNumber)
if (!OK) return Promise.reject('Login failed')
const data = await backend.getAll()
return data
}
run('197612040233')
.then(data => console.log('data', JSON.stringify(data, null, 2)))
.catch(err => console.error(err))
*/
const fetch = require('node-fetch')
const test = async () => {
const socialSecurityNumber = '197612040233'
const token = await fetch(`http://localhost:9000/login?socialSecurityNumber=${socialSecurityNumber}`, {method: 'POST'}).then(res => res.json())
// login with BankID
const {token: jwt} = await fetch(`http://localhost:9000/login/${token.order}/jwt`).then(res => res.json())
console.log('got jwt', jwt)
const headers = {authorization: 'Bearer ' + jwt}
const children = await fetch(`http://localhost:9000/children`, {headers}).then(res => res.json())
console.log('children', children)
const data = await Promise.all(children.map(async child => ({
...child,
classmates: await fetch(`http://localhost:9000/children/${child.sdsId}/classmates`, {headers}).then(res => res.json()),
news: await fetch(`http://localhost:9000/children/${child.id}/news`, {headers}).then(res => res.json()),
calendar: await fetch(`http://localhost:9000/children/${child.id}/calendar`, {headers}).then(res => res.json()),
schedule: await fetch(`http://localhost:9000/children/${child.sdsId}/schedule`, {headers}).then(res => res.json()),
menu: await fetch(`http://localhost:9000/children/${child.id}/menu`, {headers}).then(res => res.json()),
notifications: await fetch(`http://localhost:9000/children/${child.sdsId}/notifications`, {headers}).then(res => res.json()),
})))
console.log(JSON.stringify(data, null, 2))
}
test()

View File

@ -1,511 +0,0 @@
openapi: 3.0.0
servers:
- description: SwaggerHub API Auto Mocking
url: https://api.skolplattformen.org/
info:
version: "1.0.0"
title: $kolplattformen API
description: |
This is a first try to extract a usable API based on the expensive SOA crap that is called Skolplattformen in Stockholm
# Introduction
This API is a wrapper on top of the SOA layer behind **Skolplattformen** which is the mandatory platform for schools in Stockholm Stad.
# Get Started
Generate code examples from the OpenAPI yaml (press Download above) or use this as a start:
```
const socialSecurityNumber = '121212121212'
const baseUrl = 'https://api.skolplattformen.org'
const token = await fetch(`${baseUrl}/login?socialSecurityNumber=${socialSecurityNumber}`, {method: 'POST'}).then(res => res.json())
// Now start BankID and authorize, when you do - your jwt token will be ready
const jwt = await fetch(`${baseUrl}/login/${token.order}/jwt`).then(res => res.json())
const headers = {authorization: 'Bearer ' + jwt}
// Use the jwt token as bearer token in your requests
const children = await fetch(`${baseUrl}/children`, {headers}).then(res => res.json())
// Get some details
const childId = children[0].id
const child = await fetch(`${baseUrl}/children/${childId}`, {headers}).then(res => res.json())
const news = await fetch(`${baseUrl}/children/${childId}/news`, {headers}).then(res => res.json())
const calendar = await fetch(`${baseUrl}/children/${childId}/calendar`, {headers}).then(res => res.json())
```
# Open source
This project is provided AS IS and is provided free as open source. If you want to participate and add more features to this api. Please find at the repository here:
[https://github.com/kolplattformen/api](https://github.com/kolplattformen/api)
# Privacy considerations
This API encodes the cookies recieved from the backend servers as JWT tokens and send them encrypted to the client. Neither cookies or tokens are stored on the server.
# Disclaimers
I have no affiliate with the Stockholm Stad organisation or any part of any development team for the city. Therefore things may change and suddenly stop working and I have no way of knowing or even a way of contacting you. My motivation for creating this API is purely for personal reasons. I want to develop apps for my own use and have no interest to go deep in the underlying SDK every day so I'm using this API as a way of creating a little bit of sanity and conform the sometimes swinglish structure into something a little bit more consistant.
contact:
name: Christian Landgren
email: christian.landgren@iteam.se
url: https://github.com/irony
x-logo:
url: '/logo.png'
altText: $kolplattformen
paths:
/login:
post:
summary: Login
operationId: login
description: Get auth cookie for BankID login. This endpoint will initiate a login and require the user to login through BankID. When finished you will recieve a token that can be sent to the status endpoint to recieve a jwt token when authorized through BankID.
tags:
- Login
parameters:
- name: socialSecurityNumber
in: query
description: Swedish social security number connected to BankID
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Auth"
/login/{order}/jwt:
get:
summary: Wait for Jwt token
operationId: waitForToken
description: Recieve the status of the order token after the user has approved the authorization request in the BankID app, this endpoint will return a jwt token that can be used for the subsequential requests. This request will wait up until two minutes for a response.
tags:
- Login
parameters:
- name: order
in: path
description: Order token received from the /login endpoint
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/AuthToken"
'408':
description: Timeout
/children:
get:
operationId: getChildren
summary: List Children
security:
- bearerAuth: []
description: >-
Receive a list of children available through the API to the logged in user
Important to only show children you are eligble to see :)
tags:
- Children
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Children"
'401':
$ref: '#/components/responses/UnauthorizedError'
/children/{childId}:
get:
operationId: getChildById
summary: Child
description: Get all info for this this child
security:
- bearerAuth: []
tags:
- Children
parameters:
- name: childId
in: path
description: Child Id (received from /children)
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/ChildAll"
/children/{childId}/news:
get:
summary: News
operationId: getNews
description: Get list of news items for this child
tags:
- Children
parameters:
- name: childId
in: path
description: Child Id (received from /children)
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/News"
/children/{childId}/calendar:
get:
summary: Calendar
operationId: getCalendar
description: Get list of calendar events
security:
- bearerAuth: []
tags:
- Children
parameters:
- name: childId
in: path
description: Child Id (received from /children)
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Calendar"
/children/{childSdsId}/classmates:
get:
summary: Classmates
operationId: getClassmates
description: Get list of class mates and guardians in this childs class
security:
- bearerAuth: []
tags:
- Children
parameters:
- name: childSdsId
in: path
description: Child SdsId (received from /children)
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Classmates"
/children/{childSdsId}/notifications:
get:
summary: Notifications
operationId: getNotifications
description: Get list of notifications for this child
security:
- bearerAuth: []
tags:
- Children
parameters:
- name: childSdsId
in: path
description: Child sdsId (received from /children) (this is a separate id than childId for some reason, probably a subsystem)
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/Notifications"
/children/{childId}/menu:
get:
summary: Lunch Menu
operationId: getMenu
description: Get food menu for the week for this child
security:
- bearerAuth: []
tags:
- Children
parameters:
- name: childId
in: path
description: Child Id (received from /children)
required: true
schema:
type: string
responses:
'200':
description: OK
/children/{childSdsId}/schedule:
get:
summary: Schedule
operationId: getSchedule
description: Get list of news items for this child
security:
- bearerAuth: []
tags:
- Children
parameters:
- name: childSdsId
in: path
description: Child SdsId (received from /children) (this is a separate id than childId for some reason, probably a subsystem)
required: true
schema:
type: string
responses:
'200':
description: OK
/download/:
get:
summary: Download
operationId: download
description: Download content from an url using the jwt token
security:
- bearerAuth: []
tags:
- Utility
parameters:
- name: url
in: query
description: URL to download
example: /vardnadshavare/content/img.jpg
required: true
schema:
type: string
responses:
'200':
description: OK
'404':
description: Not Found
components:
schemas:
Auth:
type: object
properties:
token:
type: string
format: uuid
order:
type: string
format: uuid
AuthToken:
type: object
description: "A JWT token that should be used for authorizing requests"
properties:
token:
type: string
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
News:
type: array
items:
$ref: "#/components/schemas/NewsItem"
NewsItem:
type: object
properties:
id:
type: string
format: uuid
header:
type: string
example: "Nya direktiv från folkhälsomyndigheten"
intro:
type: string
example: "Nedan följer viktig information till dig som förälder med barn"
body:
type: string
example: "Hej\n\nNedan följer viktig information t..."
published:
type: string
example: "2020-12-24T14:00:00.966Z"
modified:
type: string
example: "2020-12-24T14:00:00.966Z"
imageUrl:
type: string
description: "A news item from the school, for example a weekly news letter"
Calendar:
type: array
items:
$ref: "#/components/schemas/CalendarItem"
CalendarItem:
type: object
properties:
id:
type: integer
format: int64
title:
type: string
example: "Tidig stängning"
description:
type: string
example: "På torsdag stänger vi 15:45 på grund av Lucia"
location:
type: string
example: ""
startDate:
type: string
format: date
example: "2020-12-13"
endDate:
type: string
format: date
example: "2020-12-13"
allDay:
type: boolean
example: true
ChildAll:
type: object
properties:
child:
$ref: "#/components/schemas/Child"
news:
$ref: "#/components/schemas/News"
calendar:
$ref: "#/components/schemas/Calendar"
notifications:
$ref: "#/components/schemas/Notifications"
Classmates:
type: array
items:
$ref: "#/components/schemas/Classmate"
Classmate:
type: object
properties:
sisId:
type: string
format: uuid
className:
type: string
description: The name of the class of this classmate
example: "8C"
firstname:
type: string
example: "Max"
lastname:
type: string
example: "Svensson"
guardians:
type: array
items:
$ref: "#/components/schemas/Guardian"
Guardian:
type: object
properties:
email:
type: string
format: email
example: allan@svensson.se
firstname:
type: string
example: Allan
lastname:
type: string
example: Svensson
mobile:
type: string
example: 070-13372121
address:
type: string
example: Vallebyvägen 13b, lgh 223A
Notifications:
type: array
items:
$ref: "#/components/schemas/Notification"
Notification:
type: object
properties:
id:
type: string
format: uuid
example: "133869-e254-487a-ac4a-2ab6e5dabeec"
sender.name:
type: string
example: "Elevdokumentation"
dateCreated:
type: string
format: date-time
example: "2020-12-10T14:31:29.966Z"
message:
type: string
example: "Nu kan du ta del av ditt barns dokumentation av utvecklingssamtal"
url:
type: string
example: "https://elevdokumentation.stockholm.se/mvvm/loa3/conference/guardian/student/802003_sthlm/documents/38383838-1111-4f3a-b022-94d24ed72b31"
description: "URL with the actual message as a webpage. Needs separate login. TODO: Investigate how to solve this somehow"
category:
type: string
example: "Lärlogg"
messageType:
type: string
example: "avisering"
Children:
type: array
items:
$ref: "#/components/schemas/Child"
Child:
type: object
properties:
id:
type: string
format: uuid
sdsId:
type: string
format: uuid
description: Special ID used to access certain subsystems
name:
type: string
example: "Kalle Svensson (elev)"
status:
type: string
description: F - förskola, GR - grundskola?
example: "F;GR"
schoolId:
type: string
format: uuid
example: "133372F4-AF59-613D-1636-543EC3652111"
responses:
UnauthorizedError:
description: Access token is missing or invalid
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
This API uses JWT tokens to authorize requests. To recieve a JWT token you have to
1. Initiate a login session through the [/login](/login) endpoint. This will trigger a BankID login.
2. After this you can query the /login/{order}/jwt endpoint which will return a JWT token when the user has finished authorizing the request in their BankID app.
The token contains a session cookie which will expire. If you receive an error message when using the expired token, please repeat the process.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
import { Col, Container, Row } from 'react-bootstrap'
import SwiperCore, { Autoplay, Pagination } from 'swiper'
import { Swiper, SwiperSlide } from 'swiper/react'
import Image from 'next/image'
import img1 from '../assets/img/feature/app-img.png'
import img2 from '../assets/img/feature/app-img2.png'
import img3 from '../assets/img/feature/app-img3.png'
@ -62,102 +63,102 @@ const AppShots = () => {
<Swiper className="app-carousel" {...swiperParams}>
<SwiperSlide>
<div className="single-app-image">
<img src={img1} alt="" />
<Image width="300" height="649" src={img1} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img2} alt="" />
<Image width="300" height="649" src={img2} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img3} alt="" />
<Image width="300" height="649" src={img3} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img4} alt="" />
<Image width="300" height="649" src={img4} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img5} alt="" />
<Image width="300" height="649" src={img5} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img1} alt="" />
<Image width="300" height="649" src={img1} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img2} alt="" />
<Image width="300" height="649" src={img2} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img3} alt="" />
<Image width="300" height="649" src={img3} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img4} alt="" />
<Image width="300" height="649" src={img4} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img5} alt="" />
<Image width="300" height="649" src={img5} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img1} alt="" />
<Image width="300" height="649" src={img1} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img2} alt="" />
<Image width="300" height="649" src={img2} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img3} alt="" />
<Image width="300" height="649" src={img3} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img4} alt="" />
<Image width="300" height="649" src={img4} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img5} alt="" />
<Image width="300" height="649" src={img5} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img1} alt="" />
<Image width="300" height="649" src={img1} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img2} alt="" />
<Image width="300" height="649" src={img2} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img3} alt="" />
<Image width="300" height="649" src={img3} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img4} alt="" />
<Image width="300" height="649" src={img4} alt="" />
</div>
</SwiperSlide>
<SwiperSlide>
<div className="single-app-image">
<img src={img5} alt="" />
<Image width="300" height="649" src={img5} alt="" />
</div>
</SwiperSlide>
</Swiper>

View File

@ -8,6 +8,7 @@ import shape4 from '../assets/img/banner/shaps4.png'
import shape5 from '../assets/img/banner/shaps5.png'
import shape6 from '../assets/img/banner/shaps6.png'
import shape7 from '../assets/img/banner/shaps7.png'
import Image from 'next/image'
import appstore from '../assets/img/appstore.svg'
import playstore from '../assets/img/playstore.png'
@ -106,7 +107,7 @@ const Banner = () => {
</Col>
<Col md={4} lg={5} className=" offset-lg-1 offse-xl-2">
<div className="banner-image">
<img src={bannerMoc} alt="" />
<Image src={bannerMoc} width="300" height="584" alt="" />
</div>
</Col>
</Row>

View File

@ -1,4 +1,5 @@
import { Col, Container, Row } from 'react-bootstrap'
import Image from 'next/image'
import img2 from '../assets/img/girls.png'
const CtaThree = () => {
@ -22,7 +23,7 @@ const CtaThree = () => {
</Col>
<Col lg={7} sm={7}>
<div className="user-interact-image type2">
<img src={img2} alt="" />
<Image src={img2} alt="" width="668" height="500" />
</div>
</Col>
</Row>

View File

@ -1,6 +1,8 @@
import { Col, Container, Row } from 'react-bootstrap'
import Image from 'next/image'
import img1 from '../assets/img/boys.png'
const CtaTwo = () => {
return (
<section className="bg-2 pt-120 pb-120">
@ -8,7 +10,7 @@ const CtaTwo = () => {
<Row>
<Col lg={7} sm={7}>
<div className="user-interact-image">
<img src={img1} alt="" />
<Image src={img1} width="668" height="500" alt="" />
</div>
</Col>
<Col lg={5} sm={5}>

View File

@ -5,7 +5,7 @@
"license": "MIT",
"dependencies": {
"bootstrap": "^4.5.0",
"next": "^9.3.2",
"next": "^10.0.6",
"next-images": "^1.4.0",
"react": "^16.13.1",
"react-bootstrap": "^1.0.1",

File diff suppressed because it is too large Load Diff