feat: first working test-app against login endpoint

This commit is contained in:
Christian Landgren 2023-09-08 15:35:17 +02:00
parent 2daa1b52fb
commit e0adb9797b
40 changed files with 2951 additions and 227 deletions

View File

@ -28,10 +28,7 @@ let bankIdUsed = false
const recordFolder = `${__dirname}/record`
async function run() {
const agent = new HttpProxyAgent('http://localhost:8080')
const agentEnabledFetch = agentWrapper(nodeFetch, agent)
const fetch = fetchCookie(agentEnabledFetch, cookieJar)
const fetch = fetchCookie(nodeFetch, cookieJar)
try {
const api = init(fetch, cookieJar, { record })

View File

@ -646,4 +646,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: f4a92b32cc4938e15ad7ccfefe9898548670abed
COCOAPODS: 1.11.2
COCOAPODS: 1.12.1

View File

@ -980,7 +980,7 @@
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@ -1042,7 +1042,7 @@
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;

View File

@ -0,0 +1,288 @@
import { wrapToughCookie } from '@skolplattformen/api'
import { CookieJar } from 'tough-cookie'
import { ApiHjarntorget } from './apiAdmentum'
const setupSuccessfullLoginInitiation = (fetcherMock: jest.Mock) => {
// 'begin-login'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
url: 'some url with url encoded at the end?return=hello',
})
)
// 'init-shibboleth-login'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
url: 'some url with url encoded at the end?Target=hello',
})
)
// 'init-bankId'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
text: jest.fn().mockReturnValue(
Promise.resolve(`
<html>
<body>
<input name="RelayState" value="aUUID"></input>
<input name="SAMLRequest" value="somebase64value"></input>
</body>
</html>`)
),
})
)
// 'pick-mvghost'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
url: 'some url to a mvghost',
})
)
// 'start-bankId'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
url: 'some base url to a mvghost to use when polling status',
})
)
}
const setupSuccessfullBankIdLogin = (fetcherMock: jest.Mock) => {
// 'poll-bankid-status'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
json: jest.fn().mockReturnValue(
Promise.resolve({
infotext: '',
location: 'an url to go to confirm the login',
})
),
})
)
// 'confirm-signature-redirect'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
text: jest.fn().mockReturnValue(
Promise.resolve(`
<html>
<body>
<textarea name="RelayState">relay state probably same uuid as before</textarea>
<textarea name="SAMLResponse">base64 encoded saml response</textarea>
</body>
</html>`)
),
})
)
// 'authgbg-saml-login'
fetcherMock.mockReturnValueOnce(
Promise.resolve({
text: jest.fn().mockReturnValue(
Promise.resolve(`
<html>
<body>
<input name="RelayState" value="aUUID"></input>
<input name="SAMLResponse" value="somebase64value"></input>
</body>
</html>`)
),
})
)
// 'admentum-saml-login'
fetcherMock.mockReturnValueOnce(Promise.resolve({ status: 200 }))
}
describe('api', () => {
let fetcherMock: jest.Mock
let api: ApiHjarntorget
beforeEach(() => {
const fetcher = jest.fn()
fetcherMock = fetcher as jest.Mock
const cookieManager = wrapToughCookie(new CookieJar())
cookieManager.clearAll()
api = new ApiHjarntorget(jest.fn(), cookieManager)
api.replaceFetcher(fetcher)
})
it('works', () => {
expect(1 + 1).toBe(2)
})
// describe('#login', () => {
// it('goes through single sing-on steps', async (done) => {
// setupSuccessfullLoginInitiation(fetcherMock)
// setupSuccessfullBankIdLogin(fetcherMock)
// const personalNumber = 'my personal number'
// const loginComplete = new Promise((resolve, reject) => {
// api.on('login', () => done())
// });
// await api.login(personalNumber)
// })
// it('checker emits PENDING', async (done) => {
// // 'poll-bankid-status'
// fetcherMock.mockReturnValueOnce(Promise.resolve({
// json: jest.fn().mockReturnValue(Promise.resolve({
// infotext: "some prompt to do signing in app",
// location: ""
// }))
// }))
// const status = checkStatus(fetcherMock, "some url")
// status.on('PENDING', () => {
// status.cancel()
// done()
// })
// })
// it('checker emits ERROR', async (done) => {
// // 'poll-bankid-status'
// fetcherMock.mockReturnValueOnce(Promise.resolve({
// json: jest.fn().mockReturnValue(Promise.resolve({
// infotext: "some prompt to do signing in app",
// location: "url with error in the name"
// }))
// }))
// const status = checkStatus(fetcherMock, "some url")
// status.on('ERROR', () => {
// status.cancel()
// done()
// })
// })
// it('checker emits ERROR when an exception occurs', async (done) => {
// // 'poll-bankid-status'
// fetcherMock.mockReturnValueOnce(Promise.resolve({
// json: jest.fn().mockReturnValue(Promise.resolve({
// infotext: undefined,
// location: undefined
// }))
// }))
// const status = checkStatus(fetcherMock, "some url")
// status.on('ERROR', () => {
// status.cancel()
// done()
// })
// })
// it('remembers used personal number', async (done) => {
// setupSuccessfullLoginInitiation(fetcherMock)
// setupSuccessfullBankIdLogin(fetcherMock)
// const personalNumber = 'my personal number'
// await api.login(personalNumber)
// api.on('login', () => {
// expect(api.getPersonalNumber()).toEqual(personalNumber)
// done()
// })
// })
// it('forgets used personal number if sign in is unsuccessful', async (done) => {
// setupSuccessfullLoginInitiation(fetcherMock)
// // 'poll-bankid-status'
// fetcherMock.mockReturnValueOnce(Promise.resolve({
// json: jest.fn().mockReturnValue(Promise.resolve({
// infotext: "",
// location: "an url to go to confirm the login"
// }))
// }))
// // 'confirm-signature-redirect'
// fetcherMock.mockReturnValueOnce(Promise.resolve({
// text: Promise.resolve("some error occured")
// }))
// const personalNumber = 'my personal number'
// const status = await api.login(personalNumber)
// status.on('ERROR', () => {
// expect(api.getPersonalNumber()).toEqual(undefined)
// done()
// })
// })
// // TODO: Possibly rewrite the mocking so we mock the responses more properly,
// // that way it would be possible to implement a throwIfNotOk wrapper for the
// // fetch calls.
// // it('throws error on external api error', async () => {
// // const personalNumber = 'my personal number'
// // try {
// // await api.login(personalNumber)
// // } catch (error: any) {
// // expect(error.message).toEqual(expect.stringContaining('Server Error'))
// // }
// // })
// })
// describe('#logout', () => {
// // it('clears session', async () => {
// // await api.logout()
// // const session = await api.getSession('')
// // expect(session).toEqual({
// // headers: {
// // cookie: '',
// // },
// // })
// // })
// it('emits logout event', async () => {
// const listener = jest.fn()
// api.on('logout', listener)
// await api.logout()
// expect(listener).toHaveBeenCalled()
// })
// it('sets .isLoggedIn', async () => {
// api.isLoggedIn = true
// await api.logout()
// expect(api.isLoggedIn).toBe(false)
// })
// it('forgets personalNumber', async () => {
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// (api as any).personalNumber = 'my personal number'
// api.isLoggedIn = true
// await api.logout()
// expect(api.getPersonalNumber()).toEqual(undefined)
// })
// })
/*
describe('fake', () => {
it('sets fake mode for the correct pnr:s', async () => {
let status
status = await api.login('121212121212')
expect(status.token).toEqual('fake')
status = await api.login('201212121212')
expect(status.token).toEqual('fake')
status = await api.login('1212121212')
expect(status.token).toEqual('fake')
})
it('delivers fake data', async (done) => {
api.on('login', async () => {
const user = await api.getUser()
expect(user).toEqual({
firstName: 'Namn',
lastName: 'Namnsson',
isAuthenticated: true,
personalNumber: "195001182046",
})
const children = await api.getChildren()
expect(children).toHaveLength(2)
const calendar1 = await api.getCalendar(children[0])
expect(calendar1).toHaveLength(20)
const calendar2 = await api.getCalendar(children[1])
expect(calendar2).toHaveLength(18)
const skola24Children = await api.getSkola24Children()
expect(skola24Children).toHaveLength(1)
const timetable = await api.getTimetable(skola24Children[0], 2021, 15, 'sv')
expect(timetable).toHaveLength(32)
done()
})
await api.login('121212121212')
})
})*/
})

View File

@ -0,0 +1,323 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Api,
CalendarItem,
Classmate,
CookieManager,
EtjanstChild,
Fetch,
Fetcher,
FetcherOptions,
FrejaLoginStatusChecker,
LoginStatusChecker,
MenuItem,
NewsItem,
Notification,
ScheduleItem,
SchoolContact,
Skola24Child,
Teacher,
TimetableEntry,
toMarkdown,
User,
wrap,
} from '@skolplattformen/api'
import { EventEmitter } from 'events'
import { decode } from 'he'
import { DateTime, FixedOffsetZone } from 'luxon'
import * as html from 'node-html-parser'
import { fakeFetcher } from './fake/fakeFetcher'
import { checkStatus, DummyStatusChecker } from './loginStatus'
import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers'
import { bankIdInitUrl, bankIdCheckUrl, apiUrls } from './routes'
import parse from '@skolplattformen/curriculum'
function getDateOfISOWeek(week: number, year: number) {
const simple = new Date(year, 0, 1 + (week - 1) * 7)
const dow = simple.getDay()
const isoWeekStart = simple
if (dow <= 4) isoWeekStart.setDate(simple.getDate() - simple.getDay() + 1)
else isoWeekStart.setDate(simple.getDate() + 8 - simple.getDay())
return isoWeekStart
}
export class ApiAdmentum extends EventEmitter implements Api {
private fetch: Fetcher
private realFetcher: Fetcher
private personalNumber?: string
private cookieManager: CookieManager
public isLoggedIn = false
private _isFake = false
public set isFake(fake: boolean) {
this._isFake = fake
if (this._isFake) {
this.fetch = fakeFetcher
} else {
this.fetch = this.realFetcher
}
}
public get isFake() {
return this._isFake
}
constructor(
fetch: Fetch,
cookieManager: CookieManager,
options?: FetcherOptions
) {
super()
this.fetch = wrap(fetch, options)
this.realFetcher = this.fetch
this.cookieManager = cookieManager
}
public replaceFetcher(fetcher: Fetcher) {
this.fetch = fetcher
}
async getSchedule(
child: EtjanstChild,
from: DateTime,
to: DateTime
): Promise<(CalendarItem & ScheduleItem)[]> {
const lessonsResponseJson: any[] = []
return lessonsResponseJson.map((l) => {
const start = DateTime.fromMillis(l.startDate.ts, {
zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes),
})
const end = DateTime.fromMillis(l.endDate.ts, {
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes),
})
return {
id: l.id,
title: l.title,
description: l.note,
location: l.location,
startDate: start.toISO(),
endDate: end.toISO(),
oneDayEvent: false,
allDayEvent: false,
}
})
}
getPersonalNumber(): string | undefined {
return this.personalNumber
}
public async getSessionHeaders(
url: string
): Promise<{ [index: string]: string }> {
const cookie = await this.cookieManager.getCookieString(url)
return {
cookie,
}
}
async setSessionCookie(sessionCookie: string): Promise<void> {
// this.cookieManager.setCookieString(sessionCookie, admentumUrl)
const user = await this.getUser()
if (!user.isAuthenticated) {
throw new Error('Session cookie is expired')
}
this.isLoggedIn = true
this.emit('login')
}
async getUser(): Promise<User> {
console.log('fetching user')
const currentUserResponse = await this.fetch('current-user', apiUrls.users) // + /id?
if (currentUserResponse.status !== 200) {
return { isAuthenticated: false }
}
const retrivedUser = await currentUserResponse.json()
return { ...retrivedUser, isAuthenticated: true }
}
async getChildren(): Promise<(Skola24Child & EtjanstChild)[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
console.log('fetching children')
const myChildrenResponseJson: any[] = []
return myChildrenResponseJson.map(
(c) =>
({
id: c.id,
sdsId: c.id,
personGuid: c.id,
firstName: c.firstName,
lastName: c.lastName,
name: `${c.firstName} ${c.lastName}`,
} as Skola24Child & EtjanstChild)
)
}
async getCalendar(child: EtjanstChild): Promise<CalendarItem[]> {
return Promise.resolve([])
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getClassmates(_child: EtjanstChild): Promise<Classmate[]> {
// TODO: We could get this from the events a child is associated with...
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return Promise.resolve([])
}
public async getTeachers(child: EtjanstChild): Promise<Teacher[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return Promise.resolve([])
}
public async getSchoolContacts(
child: EtjanstChild
): Promise<SchoolContact[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return Promise.resolve([])
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getNews(_child: EtjanstChild): Promise<NewsItem[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return Promise.resolve([])
}
async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise<any> {
return { ...item }
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getMenu(_child: EtjanstChild): Promise<MenuItem[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
// Have not found this available on hjärntorget. Perhaps do a mapping to https://www.skolmaten.se/ ?
return Promise.resolve([])
}
async getChildEventsWithAssociatedMembers(child: EtjanstChild) {
return this.getEventsWithAssociatedMembersForChildren([child])
}
async getEventsWithAssociatedMembersForChildren(children: EtjanstChild[]) {
return Promise.resolve([])
}
async getNotifications(child: EtjanstChild): Promise<Notification[]> {
return Promise.resolve([])
}
async getSkola24Children(): Promise<Skola24Child[]> {
if (!this.isLoggedIn) {
throw new Error('Not logged in...')
}
return []
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getTimetable(
child: Skola24Child,
week: number,
year: number,
_lang: string
): Promise<TimetableEntry[]> {
const startDate = DateTime.fromJSDate(getDateOfISOWeek(week, year))
const endDate = startDate.plus({ days: 7 })
const lessonsResponseJson: any[] = []
return lessonsResponseJson.map((l) => {
const start = DateTime.fromMillis(l.startDate.ts, {
zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes),
})
const end = DateTime.fromMillis(l.endDate.ts, {
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes),
})
return {
...parse(l.title, _lang),
id: l.id,
teacher: l.bookedTeacherNames && l.bookedTeacherNames[0],
location: l.location,
timeStart: start.toISOTime().substring(0, 5),
timeEnd: end.toISOTime().substring(0, 5),
dayOfWeek: start.toJSDate().getDay(),
blockName: l.title,
dateStart: start.toISODate(),
dateEnd: end.toISODate(),
} as TimetableEntry
})
}
async logout(): Promise<void> {
this.isLoggedIn = false
this.personalNumber = undefined
this.cookieManager.clearAll()
this.emit('logout')
}
public async login(personalNumber?: string): Promise<LoginStatusChecker> {
// short circut the bank-id login if in fake mode
if (personalNumber !== undefined && personalNumber.endsWith('1212121212'))
return this.fakeMode()
this.isFake = false
const sessionId = await this.fetch('init-session', bankIdInitUrl(''))
.then((res) => res.text())
.then((text) => /sessionsid=(.)/.exec(text)?.[0])
if (!sessionId) throw new Error('No session provided')
console.log('start polling', sessionId)
const statusChecker = checkStatus(this.fetch, bankIdCheckUrl(sessionId))
statusChecker.on('OK', async () => {
// setting these similar to how the sthlm api does it
// not sure if it is needed or if the cookies are enough for fetching all info...
this.isLoggedIn = true
this.personalNumber = personalNumber
this.emit('login')
})
statusChecker.on('ERROR', () => {
this.personalNumber = undefined
})
return statusChecker
}
private async fakeMode(): Promise<LoginStatusChecker> {
this.isFake = true
setTimeout(() => {
this.isLoggedIn = true
this.emit('login')
}, 50)
const emitter: any = new EventEmitter()
emitter.token = 'fake'
return emitter
}
async loginFreja(): Promise<FrejaLoginStatusChecker> {
throw new Error('Not implemented...')
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,21 @@
export const currentUser = () =>
({
url: 'https://admentum.goteborg.se/api/core/current-user',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve({
id: '889911_goteborgsstad',
firstName: 'TOLV',
lastName: 'TOLVAN',
email: null,
online: true,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
}),
} as any as Response)

View File

@ -0,0 +1,225 @@
export const eventRoleMembers21 = () =>
({
url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=21&roleId=821',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: '__system$virtual$calendar__',
firstName: 'Kalendern',
lastName: 'i PING PONG',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/default/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
// Klass: 8B
id: '133700_goteborgsstad',
firstName: 'Azra',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
]),
} as any as Response)
export const eventRoleMembers14 = () =>
({
url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=14&roleId=821',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
// Klass: 8B
id: '133700_goteborgsstad',
firstName: 'Azra',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '362119_goteborgsstad',
firstName: 'Elina',
lastName: 'Cocolis',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '999999_goteborgsstad',
firstName: 'Sanne',
lastName: 'Berggren',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '168925_goteborgsstad',
firstName: 'Teddy',
lastName: 'Karlsson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '494949_goteborgsstad',
firstName: 'Fideli',
lastName: 'Sundström',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
]),
} as any as Response)
export const eventRoleMembers18 = () =>
({
url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=18&roleId=821',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: '776655_goteborgsstad',
firstName: 'Walid',
lastName: 'Söderström',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '388601_goteborgsstad',
firstName: 'Rosa',
lastName: 'Fredriksson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '654654_goteborgsstad',
firstName: 'Moses',
lastName: 'Johansson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '1313131_goteborgsstad',
firstName: 'Haris',
lastName: 'Jonsson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '887766_goteborgsstad',
firstName: 'Neo',
lastName: 'Lundström',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
// Klass: 5A
id: '123456_goteborgsstad',
firstName: 'Jon',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
]),
} as any as Response)
export const eventRoleMembers24 = () =>
({
url: 'https://admentum.goteborg.se/api/event-members/members-having-role?eventId=24&roleId=821',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: '393939_goteborgsstad',
firstName: 'Malik Maria',
lastName: 'Henriksson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '444444_goteborgsstad',
firstName: 'Idas',
lastName: 'Svensson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '818181_goteborgsstad',
firstName: 'Nadja',
lastName: 'Ekström',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
id: '919191_goteborgsstad',
firstName: 'Karim',
lastName: 'Fakir',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
// Klass: Förskola
id: '133737_goteborgsstad',
firstName: 'Havin',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
]),
} as any as Response)

View File

@ -0,0 +1,38 @@
export const events = () =>
({
url: 'https://admentum.goteborg.se/api/events/events-sorted-by-name?offset=0&limit=100',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: 18,
name: '138JÄTS 21/22 5A',
url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D12',
state: 'ONGOING',
},
{
id: 14,
name: '138JÄTS 21/22 8B',
url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D14',
state: 'ONGOING',
},
{
id: 21,
name: '138JÄTS Provschema år 8',
url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D21',
state: 'ONGOING',
},
{
id: 24,
name: '139SS27F Södra Bangatan förskola',
url: 'https://admentum.goteborg.se/o/apiAccessWithKey.do?forwardUrl=%2FlaunchCourse.do%3Fid%3D24',
state: 'ONGOING',
},
]),
} as any as Response)

View File

@ -0,0 +1,36 @@
import { Fetcher, Response } from '@skolplattformen/api'
import { calendars, calendar_14241345 } from './calendars';
import { currentUser } from './current-user';
import { events } from './events';
import { lessons_123456_goteborgsstad, lessons_133700_goteborgsstad, lessons_133737_goteborgsstad } from './lessons';
import { myChildren } from './my-children';
import { wallEvents } from './wall-events';
import { information } from './information'
import { genericRolesInEvent } from './roles-in-event';
import { eventRoleMembers14, eventRoleMembers18, eventRoleMembers21, eventRoleMembers24 } from './event-role-members';
const fetchMappings: { [name:string]: () => Response} = {
'current-user': currentUser,
'events': events,
'my-children': myChildren,
'wall-events': wallEvents,
'lessons-133700_goteborgsstad': lessons_133700_goteborgsstad,
'lessons-133737_goteborgsstad': lessons_133737_goteborgsstad,
'lessons-123456_goteborgsstad': lessons_123456_goteborgsstad,
'info': information,
'roles-in-event-14': genericRolesInEvent,
'roles-in-event-18': genericRolesInEvent,
'roles-in-event-21': genericRolesInEvent,
'roles-in-event-24': genericRolesInEvent,
'event-role-members-14-821': eventRoleMembers14,
'event-role-members-18-821': eventRoleMembers18,
'event-role-members-21-821': eventRoleMembers21,
'event-role-members-24-821': eventRoleMembers24,
'calendars': calendars,
'calendar-14241345': calendar_14241345,
}
export const fakeFetcher: Fetcher = (name: string, url: string, init?: any): Promise<Response> => {
const responder = fetchMappings[name] ?? (() => {throw new Error("Request not faked for name: " + name)})
return Promise.resolve(responder());
}

View File

@ -0,0 +1,118 @@
/* eslint-disable no-useless-escape */
export const information = () =>
({
url: 'https://admentum.goteborg.se/api/information/messages-by-date-desc?messageStatus=CURRENT&offset=0&limit=10&language=en',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: 3276034,
title: 'Nu får du och ditt barn tillgång till Polyglutt hemma',
body: '<p><strong>Nu f&aring;r alla barn som g&aring;r i kommunal f&ouml;rskola i G&ouml;teborg tillg&aring;ng till bilderboksappen Polyglutt hemifr&aring;n! Det inneb&auml;r att du som v&aring;rdnadshavare och barn kan ta del av ett bibliotek av b&ouml;cker p&aring; b&aring;de svenska och 60 andra spr&aring;k, inklusive TAKK och teckenspr&aring;k via telefon eller l&auml;splatta.</strong></p>\r\n<p>Polyglutt &auml;r en app med bilderb&ouml;cker som fungerar som ett verktyg f&ouml;r att arbeta med spr&aring;kutveckling och litteratur i f&ouml;rskolan och hemma.</p>\r\n<p>Polyglutt Home Access &auml;r en tj&auml;nst som inneb&auml;r att alla barn som g&aring;r i kommunal f&ouml;rskola i G&ouml;teborg f&aring;r tillg&aring;ng till ett bibliotek av b&ouml;cker p&aring; b&aring;de svenska och 60 andra spr&aring;k, inklusive TAKK och teckenspr&aring;k hemifr&aring;n. Varje f&ouml;rskola kan ocks&aring; skapa egna bokhyllor med boktips i appen som du och ditt barn kan l&auml;sa hemma.</p>\r\n<p>Tj&auml;nsten fungerar p&aring; iPad, Androidplattor och i mobilen.</p>\r\n<p>Vill du veta mer om tj&auml;nsten, kontakta pedagogerna p&aring; ditt barns f&ouml;rskola.</p>',
creator: {
id: '501747_goteborgsstad',
firstName: 'Information Digitalisering',
lastName: 'Innovation',
email:
'information.digitaliseringochinnovation@forskola.goteborg.se',
online: false,
imagePath:
'/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
recipientGroups: [
{
id: 1121821,
name: 'DL Göteborg Vhavare förskolor',
},
],
created: {
ts: 1629970713111,
timezoneOffsetMinutes: 120,
},
attachments: [],
readByUser: false,
archivedByUser: false,
},
{
id: 3270718,
title: 'Information från grundskoleförvaltningen',
body: '<p>Till v&aring;rdnadshavare med barn p&aring; G&ouml;teborgs Stads grundskolor och grunds&auml;rskolor.</p>\r\n<p>Spridningen av covid-19 har &ouml;kat. D&auml;rf&ouml;r &auml;r det viktigt att alla hj&auml;lper till att minska spridningen av smitta.</p>\r\n<h2>Vi forts&auml;tter h&aring;lla avst&aring;nd</h2>\r\n<ul>\r\n<li>Om du vill ha kontakt med n&aring;gon p&aring; ditt barns skola vill vi g&auml;rna att du ringer eller skickar e-post.</li>\r\n<li>L&auml;mna och h&auml;mta ditt barn utomhus p&aring; skolg&aring;rden.</li>\r\n<li>En del m&ouml;ten som skolan har kommer att vara digitala.</li>\r\n<li>Uppmuntra ditt barn att promenera till och fr&aring;n skolan f&ouml;r att minska tr&auml;ngseln i kollektivtrafiken.</li>\r\n</ul>\r\n<h2>Detta g&auml;ller n&auml;r ditt barn &auml;r sjukt</h2>\r\n<ul>\r\n<li>Barn som bara &auml;r lite sjuka, som till exempel &auml;r snuviga eller har ont i halsen, ska stanna hemma.</li>\r\n<li>Ber&auml;tta alltid f&ouml;r skolan om ditt barn har konstaterad covid-19.</li>\r\n</ul>\r\n<p><a href="https://goteborg.se/wps/wcm/connect/a515d17c-7078-4663-8493-d1900b78cfb3/Om+ditt+barn+%C3%A4r+sjukt+eller+borta+fr%C3%A5n+skolan_information+till+v%C3%A5rdnadshavare_uppdaterad+13+augusti+2021.pdf?MOD=AJPERES">H&auml;r hittar du mer information om vad som g&auml;ller n&auml;r ditt barn &auml;r sjukt.</a></p>\r\n<h2>Om ditt barn har varit p&aring; resa utomlands</h2>\r\n<p>Folkh&auml;lsomyndigheten rekommenderar alla som har varit i l&auml;nder utanf&ouml;r Norden att ta ett test f&ouml;r covid-19 n&auml;r de kommer tillbaka Sverige. Detta g&auml;ller oavsett om man har symtom eller inte.</p>\r\n<p>L&auml;s mer p&aring; Krisinformation.se om vad som g&auml;ller f&ouml;r resor fr&aring;n olika l&auml;nder: <br /><a href="https://www.krisinformation.se/detta-kan-handa/handelser-och-storningar/20192/myndigheterna-om-det-nya-coronaviruset/reseinformation-med-anledning-av-det-nya-coronaviruset">Utrikesresor och att vistas utomlands - Krisinformation.se</a></p>\r\n<h2>Undervisning p&aring; skolan</h2>\r\n<p>Fr&aring;n och med h&ouml;stterminen 2021 har alla skolor undervisning p&aring; plats i skolan. Detta g&auml;ller &auml;ven f&ouml;r &aring;rskurs 7-9.</p>\r\n<p>F&ouml;r f&ouml;rskoleklass till och med &aring;rskurs 9 finns det fortfarande m&ouml;jlighet att f&aring; undervisning p&aring; distans om:</p>\r\n<ul>\r\n<li>M&aring;nga av de som jobbar p&aring; skolan &auml;r fr&aring;nvarande p&aring; grund av covid-19 och det inte g&aring;r att ha undervisning i skolan.</li>\r\n<li>Det &auml;r stor spridningen av covid-19 bland elever och medarbetare.</li>\r\n</ul>\r\n<h2>Nytt test f&ouml;r covid-19 p&aring; skolorna</h2>\r\n<p>Inom kort b&ouml;rjar V&auml;stra G&ouml;talandsregionen med ett nytt test f&ouml;r covid-19 riktat mot elever. &nbsp;Om ditt barn har haft n&auml;ra kontakt med en person p&aring; skolan som har konstaterad covid-19 f&aring;r ni med ett paket hem med ett test.&nbsp;</p>\r\n<p>Du som v&aring;rdnadshavare hj&auml;lper ditt barn att ta testet. Testet l&auml;mnar du som v&aring;rdnadshavare sedan till en utvald v&aring;rdcentral.</p>\r\n<p>Om ditt barn ska ta ett test f&aring;r du mer information fr&aring;n ditt barns skola om hur testet g&aring;r till och vilken v&aring;rdcentral du ska l&auml;mna det till.</p>\r\n<h2>Kontakt</h2>\r\n<p>Har du fr&aring;gor eller funderingar kontaktar du ditt barns skola.</p>\r\n<p><a href="https://goteborg.se/wps/portal/press-och-media/aktuelltarkivet/aktuellt/18b9930e-d34c-4d6a-817a-c1b8e74e5f9f#Z7_42G01J41KGV2F0ALK2K1SN1M75">L&auml;s mer om covid-19 och vad som g&auml;ller f&ouml;r grundskolef&ouml;rvaltningen.</a></p>\r\n<p>&nbsp;</p>',
creator: {
id: '486497_goteborgsstad',
firstName: 'Grundskola',
lastName: 'Informerar',
email: null,
online: false,
imagePath:
'/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
recipientGroups: [
{
id: 4925595,
name: 'DL Göteborg Grundskola Vhavare Alla',
},
{
id: 4525636,
name: 'Grundskola - informationskonto',
},
{
id: 4925600,
name: 'DL Göteborg Grundsärskola Vhavare Alla',
},
],
created: {
ts: 1629096850743,
timezoneOffsetMinutes: 120,
},
attachments: [
{
id: 67888219,
name: 'Om ditt barn är sjukt eller borta från skolan_information till vårdnadshavare_uppdaterad 13 augusti 2021.pdf',
size: 70466,
},
],
readByUser: false,
archivedByUser: false,
},
{
id: 2982365,
title: 'Nya regler för skolplacering i förskoleklass och grundskola',
body: '<p>Grundskolen&auml;mnden har beslutat om nya regler f&ouml;r skolplacering i f&ouml;rskoleklass och grundskola. Reglerna ska st&auml;rka elevernas r&auml;tt till en skola n&auml;ra hemmet och b&ouml;rjar g&auml;lla 1 januari 2021.</p>\r\n<p>Du kan l&auml;sa mer p&aring; sidan <a href="https://goteborg.se/wps/portal/press-och-media/aktuelltarkivet/aktuellt/e45ce367-4d46-48b4-936d-900a3e45e490">Nya regler f&ouml;r skolplacering i f&ouml;rskoleklass och grundskola</a>.&nbsp;</p>\r\n<p>Om du har fr&aring;gor kan du kontakta grundskolef&ouml;rvaltningen p&aring; telefon: 031-365 09 60 eller e-post:&nbsp;<a href="mailto:grundskola@grundskola.goteborg.se">grundskola@grundskola.goteborg.se</a>.&nbsp;</p>\r\n<p><em>Observera att detta meddelande inte g&aring;r att svara p&aring;.&nbsp;</em></p>\r\n<p>&nbsp;</p>',
creator: {
id: '486497_goteborgsstad',
firstName: 'Grundskola',
lastName: 'Informerar',
email: null,
online: false,
imagePath:
'/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
recipientGroups: [
{
id: 4925595,
name: 'DL Göteborg Grundskola Vhavare Alla',
},
{
id: 4525636,
name: 'Grundskola - informationskonto',
},
],
created: {
ts: 1603974943027,
timezoneOffsetMinutes: 60,
},
attachments: [],
readByUser: false,
archivedByUser: false,
},
]),
} as any as Response)

View File

@ -0,0 +1,431 @@
import { toNamespacedPath } from 'path'
// TODO: fix the startDate/endDate of all lessons
export const lessons_133700_goteborgsstad = () => {
const baseTime = 1636357800000
const baseDate = new Date(baseTime)
const today = new Date()
const currentHour = today.getHours()
today.setHours(baseDate.getHours())
today.setMinutes(baseDate.getMinutes())
today.setSeconds(0)
let offset = Math.abs(baseTime - today.getTime())
const weekDay = today.getDay()
if (weekDay == 6 || (weekDay == 5 && currentHour >= 18))
offset = offset + 2 * 86400000
if (weekDay == 0) offset = offset + 86400000
if (weekDay > 0 && weekDay < 6 && currentHour >= 18)
offset = offset + 86400000
return {
url: 'https://admentum.goteborg.se/api/schema/lessons?forUser=133700_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: '36080472:1',
title: 'HKK',
location: 'A402',
calendars: ['138JÄTS 21/22 8B/HKK'],
startDate: {
ts: offset + 1636357800000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636360500000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Noel Nyström (NNM)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080497:1',
title: 'BL',
location: 'B260',
calendars: ['138JÄTS 21/22 8B/BL'],
startDate: {
ts: offset + 1636361700000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636365000000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Joseph Ekström (JHE)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '37164864:1',
title: 'IDH',
location: 'IDH Ute',
calendars: ['138JÄTS 21/22 8B/IDH'],
startDate: {
ts: offset + 1636365600000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636369800000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Katja Fransson (KAF)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080557:1',
title: 'LUNCH',
location: '-',
calendars: ['138JÄTS 21/22 8B'],
startDate: {
ts: offset + 1636370700000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636372800000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: [],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080576:1',
title: 'EN',
location: 'A402',
calendars: ['138JÄTS 21/22 8B/EN'],
startDate: {
ts: offset + 1636372800000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636376400000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Henrietta Fransson (HAF)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080591:1',
title: 'MA',
location: 'A402',
calendars: ['138JÄTS 21/22 8B/MA'],
startDate: {
ts: offset + 1636377000000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636380600000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Amin Månsson (ANM)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
]),
} as any as Response
}
export const lessons_123456_goteborgsstad = () => {
const baseTime = 1636357800000
const baseDate = new Date(baseTime)
const today = new Date()
const currentHour = today.getHours()
today.setHours(baseDate.getHours())
today.setMinutes(baseDate.getMinutes())
today.setSeconds(0)
let offset = Math.abs(baseTime - today.getTime())
const weekDay = today.getDay()
if (weekDay == 6 || (weekDay == 5 && currentHour >= 18))
offset = offset + 2 * 86400000
if (weekDay == 0) offset = offset + 86400000
if (weekDay > 0 && weekDay < 6 && currentHour >= 18)
offset = offset + 86400000
return {
url: 'https://admentum.goteborg.se/api/schema/lessons?forUser=123456_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () => [
{
id: '36080454:1',
title: 'EV',
location: 'P18',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636355400000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636357500000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Petra Modin (PMO)', 'Joakim Ness (JNE)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080467:1',
title: 'MENT',
location: 'P18',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636357500000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636358100000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Petra Modin (PMO)', 'Joakim Ness (JNE)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080474:1',
title: 'EN',
location: 'P18',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636358400000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636362000000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Petra Modin (PMO)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080502:1',
title: 'SV',
location: 'P18',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636362900000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636366500000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Joakim Ness (JNE)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080529:1',
title: 'LUNCH',
location: '-',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636366500000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636368300000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: [],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080545:1',
title: 'MA',
location: 'P18',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636369200000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636372800000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Ali Gupta (AGU)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
{
id: '36080578:1',
title: 'NO',
location: 'P18',
calendars: ['138JÄTS 21/22 5A'],
startDate: {
ts: offset + 1636373400000,
timezoneOffsetMinutes: 60,
},
endDate: {
ts: offset + 1636376400000,
timezoneOffsetMinutes: 60,
},
ownPlannings: null,
teacherPlannings: null,
teacherAndStudentPlannings: null,
ownGeneralPlannings: null,
teacherGeneralPlannings: null,
teacherAndStudentGeneralPlannings: null,
bookedResourceNames: [],
bookedTeacherNames: ['Ali Gupta (AGU)'],
hasTest: false,
hasHomework: false,
hasAssignment: false,
url: null,
note: '',
},
],
} as any as Response
}
export const lessons_133737_goteborgsstad = () =>
({
url: 'https://admentum.goteborg.se/api/schema/lessons?forUser=133737_goteborgsstad&startDateIso=2021-11-01&endDateIso=2021-11-08',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () => Promise.resolve([] as any[]),
} as any as Response)

View File

@ -0,0 +1,44 @@
export const myChildren = () =>
({
url: 'https://admentum.goteborg.se/api/person/children',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
// Klass: Förskola
id: '133737_goteborgsstad',
firstName: 'Havin',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
// Klass: 8B
id: '133700_goteborgsstad',
firstName: 'Azra',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
{
// Klass: 5A
id: '123456_goteborgsstad',
firstName: 'Jon',
lastName: 'Göransson',
email: null,
online: false,
imagePath: '/pp/lookAndFeel/skins/admentum/icons/monalisa_large.png',
extraInfoInCatalog: '',
},
]),
} as any as Response)

View File

@ -0,0 +1,18 @@
export const genericRolesInEvent = () =>
({
url: 'https://admentum.goteborg.se/api/event-members/roles?eventId=XXX&language=en',
headers: {
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36',
cookie: 'REMOVED',
},
status: 200,
statusText: '200',
json: () =>
Promise.resolve([
{
id: 821,
name: 'SINGLE ROLE',
},
]),
} as any as Response)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
import { Features } from '@skolplattformen/api'
export const features: Features = {
LOGIN_BANK_ID_SAME_DEVICE_WITHOUT_ID: false,
LOGIN_FREJA_EID: false,
FOOD_MENU: false,
CLASS_LIST: false,
}

View File

@ -0,0 +1,25 @@
import {
Api,
Fetch,
FetcherOptions,
RNCookieManager,
ToughCookieJar,
wrapReactNativeCookieManager,
wrapToughCookie,
} from '@skolplattformen/api'
import { ApiAdmentum } from './apiAdmentum'
export { features } from './features'
const init = (
fetchImpl: Fetch,
cookieManagerImpl: RNCookieManager | ToughCookieJar,
options?: FetcherOptions
): Api => {
// prettier-ignore
const cookieManager = ((cookieManagerImpl as RNCookieManager).get)
? wrapReactNativeCookieManager(cookieManagerImpl as RNCookieManager)
: wrapToughCookie(cookieManagerImpl as ToughCookieJar)
return new ApiAdmentum(fetchImpl as any, cookieManager, options)
}
export default init

View File

@ -0,0 +1,61 @@
import { Fetcher, LoginStatusChecker } from '@skolplattformen/api'
import { EventEmitter } from 'events'
import { bankIdCheckUrl } from './routes'
export class GrandidChecker extends EventEmitter implements LoginStatusChecker {
private fetcher: Fetcher
private basePollingUrl: string
public token: string
private cancelled = false
constructor(fetcher: Fetcher, basePollingUrl: string) {
super()
this.token = '' // not used, but needed for compatability with the LoginStatusChecker
this.fetcher = fetcher
this.basePollingUrl = basePollingUrl
this.check()
}
async check(): Promise<void> {
// try {
// console.log('polling bankid signature')
// // https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus
// if (true)
// this.emit('OK')
// } else if (isError) {
// console.log('polling error')
// this.emit('ERROR')
// } else if (!this.cancelled && keepPolling) {
// console.log('keep on polling...')
// this.emit('PENDING')
// setTimeout(() => this.check(), 3000)
// }
// } catch (er) {
// console.log('Error validating login to Hjärntorget', er)
// this.emit('ERROR')
// }
}
async cancel(): Promise<void> {
this.cancelled = true
}
}
export const checkStatus = (
fetch: Fetcher,
basePollingUrl: string
): LoginStatusChecker => new GrandidChecker(fetch, basePollingUrl)
export class DummyStatusChecker
extends EventEmitter
implements LoginStatusChecker
{
token = ''
async cancel(): Promise<void> {
// do nothing
}
}

View File

@ -0,0 +1 @@
declare module 'h2m'

View File

@ -0,0 +1,58 @@
import * as html from 'node-html-parser'
import { decode } from 'he'
// TODO: Move this into the parse folder and convert it to follow the pattern of other parsers (include tests).
export const extractInputField = (sought: string, attrs: string[]) => {
// there must be a better way to do this...
const s = attrs.find(e => e.indexOf(sought) >= 0) || ""
const v = s.substring(s.indexOf('value="') + 'value="'.length)
return v.substring(0, v.length - 2)
}
export function extractMvghostRequestBody(initBankIdResponseText: string) {
const doc = html.parse(decode(initBankIdResponseText))
const inputAttrs = doc.querySelectorAll('input').map(i => (i as any).rawAttrs)
const relayState = extractInputField('RelayState', inputAttrs)
const samlRequest = extractInputField("SAMLRequest", inputAttrs)
const mvghostRequestBody = `RelayState=${encodeURIComponent(relayState)}&SAMLRequest=${encodeURIComponent(samlRequest)}`
return mvghostRequestBody
}
export function extractHjarntorgetSAMLLogin(authGbgLoginResponseText: string) {
const authGbgLoginDoc = html.parse(decode(authGbgLoginResponseText))
const inputAttrs = authGbgLoginDoc.querySelectorAll('input').map(i => (i as any).rawAttrs)
const RelayStateText = extractInputField('RelayState', inputAttrs)
const SAMLResponseText = extractInputField("SAMLResponse", inputAttrs)
return `SAMLResponse=${encodeURIComponent(SAMLResponseText || '')}&RelayState=${encodeURIComponent(RelayStateText || '')}`
}
export function extractAuthGbgLoginRequestBody(signatureResponseText: string) {
const signatureResponseDoc = html.parse(decode(signatureResponseText))
const signatureResponseTextAreas = signatureResponseDoc.querySelectorAll('textarea')
const SAMLResponseElem = signatureResponseTextAreas.find(ta => {
const nameAttr = ta.getAttribute("name")
return nameAttr === 'SAMLResponse'
})
const SAMLResponseText = SAMLResponseElem?.rawText
const RelayStateElem = signatureResponseTextAreas.find(ta => {
const nameAttr = ta.getAttribute("name")
return nameAttr === 'RelayState'
})
const RelayStateText = RelayStateElem?.rawText
const authGbgLoginBody = `SAMLResponse=${encodeURIComponent(SAMLResponseText || '')}&RelayState=${encodeURIComponent(RelayStateText || '')}`
return authGbgLoginBody
}
export const parseCalendarItem = (x: html.HTMLElement): { id: number; title: string; startDate: string; endDate: string } => {
const info = Array.from(x.querySelectorAll('a'))
// TODO: the identifier is realy on this format: '\d+:\d+' currently we only take the first part so Id will clash between items
const id = info[0].getAttribute("onClick")?.replace(new RegExp("return viewEvent\\('(\\d+).+"), "$1") || NaN
const day = info[1].textContent
const timeSpan = info[2].textContent
const [startTime, endTime] = timeSpan.replace(".", ":").split("-")
return { id: +id, title: info[0].textContent, startDate: `${day} ${startTime}`, endDate: `${day} ${endTime}` }
}

View File

@ -0,0 +1,50 @@
const baseUrl = 'https://skola.admentum.se/api/v1/'
export const apiUrls = {
assignments: baseUrl + 'assignments',
attendance_summary_users: baseUrl + 'attendance/summary/users',
course_sections: baseUrl + 'course_sections',
courses: baseUrl + 'courses',
forecast_collections: baseUrl + 'forecast_collections',
forecasts: baseUrl + 'forecasts',
grade_permissions: baseUrl + 'grade_permissions',
grades: baseUrl + 'grades',
gymnasium_courses: baseUrl + 'gymnasium_courses',
leisure_group_enrollments: baseUrl + 'leisure_group_enrollments',
leisure_groups: baseUrl + 'leisure_groups',
lesson_infos: baseUrl + 'lesson_infos',
lessons: baseUrl + 'lessons',
organisations: baseUrl + 'organisations',
orientations: baseUrl + 'orientations',
permission_groups: baseUrl + 'permission_groups',
primary_group_enrollments: baseUrl + 'primary_group_enrollments',
primary_group_municipality_statistics:
baseUrl + 'primary_groups/municipality_statistic',
primary_groups: baseUrl + 'primary_groups',
program_courses: baseUrl + 'program_courses',
programs: baseUrl + 'programs',
reviews: baseUrl + 'reviews',
rooms: baseUrl + 'rooms',
schedule_breaks: baseUrl + 'schedule_breaks',
schedule_event_instances: baseUrl + 'schedule_event_instances',
schedule_events: baseUrl + 'schedule_events',
schedule_group_enrollments: baseUrl + 'schedule_group_enrollments',
schedule_group_teacher_enrollments:
baseUrl + 'schedule_group_teacher_enrollments',
schedule_groups: baseUrl + 'schedule_groups',
schedules: baseUrl + 'schedules',
school_enrollments: baseUrl + 'school_enrollments',
school_years: baseUrl + 'school_years',
schools: baseUrl + 'schools',
sickness: baseUrl + 'sickness',
subjects: baseUrl + 'subjects',
teachers: baseUrl + 'teachers',
upper_secondary_subjects: baseUrl + 'upper_secondary_subjects',
users: baseUrl + 'users',
}
export const bankIdCheckUrl = (sessionId: string) =>
`https://login.grandid.com/?sessionid=${sessionId}&eleg=1&bankid=1`
export const bankIdInitUrl = (returnUrl: string) =>
`https://auth.admentum.se/larande${returnUrl ? `?next=${returnUrl}` : ''}`

14
libs/api-admentum/run.js Normal file
View File

@ -0,0 +1,14 @@
const Admentum = require('./lib/index.ts')
const nodeFetch = require('node-fetch')
const { CookieJar } = require('tough-cookie')
const fetchCookie = require('fetch-cookie/node-fetch')
const cookieJar = new CookieJar()
const fetch = fetchCookie(nodeFetch, cookieJar)
const admentum = new Admentum(fetch, {})
const run = async () => {
const sessionId = await admentum.login('7612040233')
}
run()

View File

@ -1,20 +0,0 @@
import { ApiAdmentum } from './api'
describe('api', () => {
let api: ApiAdmentum
beforeEach(() => {
api = new ApiAdmentum()
})
test('should request and return calendar items', async () => {
expect((await api.getCalendar())[0]).toMatchObject({
allDay: false,
endDate: '2023-08-07T07:30:00.000Z',
id: 2990834,
location: '',
startDate: '2023-08-07T06:00:00.000Z',
title: 'Matematik',
})
})
})

View File

@ -1,32 +0,0 @@
import { EventEmitter } from 'events'
import { CalendarItem } from '@skolplattformen/api'
import * as fake from './fakeData'
import { parseDate } from './parse'
const fakeResponse = <T>(data: T): Promise<T> =>
new Promise((res) => setTimeout(() => res(data), 200 + Math.random() * 800))
export class ApiAdmentum extends EventEmitter {
public async getCalendar(): Promise<CalendarItem[]> {
const events = await fakeResponse(fake.calendar)
return events.map(
({
id,
title,
start_date: startDate,
end_date: endDate,
schedule_event: { start_time: startTime, end_time: endTime },
}: any) => ({
id,
title,
location: '',
allDay: startTime === '00:00:00',
startDate: parseDate(startDate + 'T' + startTime),
endDate: parseDate(endDate + 'T' + endTime),
})
)
}
}

View File

@ -1,32 +0,0 @@
export const calendar = [
{
url: 'https://skola.admentum.se/api/v1/schedule_event_instances/2990834/?format=api',
id: 2990834,
school_id: 824,
start_date: '2023-08-07',
end_date: '2023-08-07',
schedule_event: {
url: 'https://skola.admentum.se/api/v1/schedule_events/148722/?format=api',
id: 148722,
eid: null,
schedule_id: 4385,
start_time: '08:00:00',
end_time: '09:30:00',
rooms: [],
teachers: [
{
url: 'https://skola.admentum.se/api/v1/users/437302/?format=api',
id: 437302,
},
],
schedule_groups: [],
primary_groups: [
{
url: 'https://skola.admentum.se/api/v1/primary_groups/36874/?format=api',
id: 36874,
},
],
weekly_interval: '',
},
},
]

View File

@ -1,7 +0,0 @@
export const parseDate = (input?: string): string | undefined => {
if (!input) {
return undefined
}
return new Date(input).toISOString()
}

View File

@ -1,124 +0,0 @@
function requestLogger(httpModule) {
var original = httpModule.request
httpModule.request = function (options, callback) {
console.log('-----------------------------------------------')
console.log(
options.href || options.proto + '://' + options.host + options.path,
options.method
)
console.log(options.headers)
console.log('-----------------------------------------------')
return original(options, callback)
}
}
requestLogger(require('http'))
requestLogger(require('https'))
const { DateTime } = require('luxon')
const nodeFetch = require('node-fetch')
const { CookieJar } = require('tough-cookie')
const fetchCookie = require('fetch-cookie/node-fetch')
const { writeFile } = require('fs/promises')
const path = require('path')
const fs = require('fs')
const { inspect } = require('util')
const init = require('@skolplattformen/api-admentum').default
const [, , personalNumber] = process.argv
if (!personalNumber) {
console.error(
'You must pass in a valid personal number, eg `node run 197001011111`'
)
process.exit(1)
}
function ensureDirectoryExistence(filePath) {
var dirname = path.dirname(filePath)
if (fs.existsSync(dirname)) {
return true
}
ensureDirectoryExistence(dirname)
fs.mkdirSync(dirname)
}
const record = async (info, data) => {
const name = info.error ? `${info.name}_error` : info.name
const filename = `./record/${name}.json`
ensureDirectoryExistence(filename)
const content = {
url: info.url,
headers: info.headers,
status: info.status,
statusText: info.statusText,
}
if (data) {
switch (info.type) {
case 'json':
content.json = data
break
case 'text':
content.text = data
break
case 'blob':
// eslint-disable-next-line no-case-declarations
const buffer = await data.arrayBuffer()
content.blob = Buffer.from(buffer).toString('base64')
break
}
} else if (info.error) {
const { message, stack } = info.error
content.error = {
message,
stack,
}
}
await writeFile(filename, JSON.stringify(content, null, 2))
}
async function run() {
const cookieJar = new CookieJar()
const fetch = fetchCookie(nodeFetch, cookieJar)
try {
const api = init(fetch, cookieJar, { record })
console.log('inited...')
api.on('login', async () => {
console.log('Logged in!')
await api.getUser()
const children = await api.getChildren()
const now = DateTime.fromJSDate(new Date())
for (let i = 0; i < children.length; i++) {
const c = children[i]
await api.getCalendar(c)
await api.getNotifications(c)
await api.getTimetable(c, 44, 2021, 'ignored')
}
const news = await api.getNews()
// const news = await api.getNews()
// //console.table(news.map(n => ({ id: n.id, author: n.author, published: n.published})))
// //news.length && console.log(news[0])
// const notifications = await api.getNotifications(children[2])
// //const ns = notifications.map(n => ({id: n.id, sender: n.sender, type: n.type}))
// //console.table(ns)
// console.log("notifications count", notifications.length)
// notifications.slice(0, 10).forEach(console.log)
// await api.getCalendar(children[1])
// await api.getTimetable(children[1], 38, 2021, "en")
// await api.getClassmates()
// console.table(schema)
})
const res = await api.login(personalNumber)
console.log(res)
} catch (err) {
console.error(err)
}
}
run()

View File

@ -15,6 +15,7 @@
"baseUrl": ".",
"paths": {
"@skolplattformen/api": ["libs/api/lib/index.ts"],
"@skolplattformen/api-admentum": ["libs/api-admentum/lib/index.ts"],
"@skolplattformen/api-hjarntorget": ["libs/api-hjarntorget/lib/index.ts"],
"@skolplattformen/api-skolplattformen": [
"libs/api-skolplattformen/lib/index.ts"

View File

@ -1,14 +1,14 @@
{
"version": 2,
"projects": {
"api-hjarntorget": "libs/api-hjarntorget",
"api-admentum": "libs/api-admentum",
"api-skolplattformen": "libs/api-skolplattformen",
"api-vklass": "libs/api-vklass",
"api": "libs/api",
"api-admentum": "libs/api-admentum",
"api-hjarntorget": "libs/api-hjarntorget",
"api-skolplattformen": "libs/api-skolplattformen",
"api-test-app": "apps/api-test-app",
"api-vklass": "libs/api-vklass",
"curriculum": "libs/curriculum",
"hooks": "libs/hooks",
"api-test-app": "apps/api-test-app",
"skolplattformen-app": "apps/skolplattformen-app"
}
}