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:
parent
823818bc92
commit
fbbf6feaf1
|
@ -5,6 +5,7 @@
|
|||
"lerna": "^3.22.1"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "lerna run test --stream"
|
||||
"bootstrap": "npx lerna bootstrap",
|
||||
"test": "npx lerna run test --stream"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
node_modules
|
|
@ -1 +0,0 @@
|
|||
v14.15.0
|
|
@ -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
|
|
@ -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```
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"verbose": false,
|
||||
"ignore": ["*.test.js", "fixtures/*", "requests/*"]
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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 |
|
@ -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()
|
|
@ -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
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue