fix: fetch typings
This commit is contained in:
parent
8c41a06970
commit
97e131d30d
|
@ -1,78 +1,78 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { DateTime, FixedOffsetZone } from 'luxon'
|
||||
import { EventEmitter } from 'events'
|
||||
import * as html from 'node-html-parser'
|
||||
import { decode } from 'he'
|
||||
import { toMarkdown, Api, URLSearchParams, LoginStatusChecker, CalendarItem,
|
||||
import {
|
||||
Api,
|
||||
CalendarItem,
|
||||
Classmate,
|
||||
CookieManager,
|
||||
EtjanstChild,
|
||||
Fetch,
|
||||
Fetcher,
|
||||
FetcherOptions,
|
||||
LoginStatusChecker,
|
||||
MenuItem,
|
||||
NewsItem,
|
||||
Notification,
|
||||
ScheduleItem,
|
||||
Skola24Child,
|
||||
TimetableEntry,
|
||||
toMarkdown,
|
||||
URLSearchParams,
|
||||
User,
|
||||
Fetcher,
|
||||
FetcherOptions,
|
||||
wrap
|
||||
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 } from './loginStatus'
|
||||
import { extractMvghostRequestBody, parseCalendarItem } from './parse/parsers'
|
||||
import {
|
||||
beginLoginUrl,
|
||||
beginBankIdUrl,
|
||||
beginLoginUrl,
|
||||
calendarEventUrl,
|
||||
calendarsUrl,
|
||||
currentUserUrl,
|
||||
initBankIdUrl,
|
||||
fullImageUrl,
|
||||
hjarntorgetEventsUrl,
|
||||
hjarntorgetUrl,
|
||||
infoSetReadUrl,
|
||||
infoUrl,
|
||||
initBankIdUrl,
|
||||
lessonsUrl,
|
||||
membersWithRoleUrl,
|
||||
mvghostUrl,
|
||||
myChildrenUrl,
|
||||
shibbolethLoginUrlBase,
|
||||
rolesInEventUrl,
|
||||
shibbolethLoginUrl,
|
||||
shibbolethLoginUrlBase,
|
||||
verifyUrlBase,
|
||||
wallMessagesUrl,
|
||||
calendarsUrl,
|
||||
calendarEventUrl
|
||||
} from './routes'
|
||||
import { fakeFetcher } from './fake/fakeFetcher'
|
||||
|
||||
|
||||
function getDateOfISOWeek(week: number, year: number,) {
|
||||
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())
|
||||
if (dow <= 4) ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1)
|
||||
else ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay())
|
||||
return ISOweekStart
|
||||
}
|
||||
|
||||
|
||||
export class ApiHjarntorget extends EventEmitter implements Api {
|
||||
private fetch: Fetcher
|
||||
private realFetcher: Fetcher
|
||||
private realFetcher: Fetcher
|
||||
|
||||
private personalNumber?: string
|
||||
|
||||
private cookieManager: CookieManager
|
||||
|
||||
public isLoggedIn: boolean = false
|
||||
public isLoggedIn = false
|
||||
|
||||
private _isFake: boolean = false;
|
||||
private _isFake = false
|
||||
|
||||
public set isFake(fake: boolean) {
|
||||
this._isFake = fake
|
||||
if(this._isFake) {
|
||||
if (this._isFake) {
|
||||
this.fetch = fakeFetcher
|
||||
} else {
|
||||
this.fetch = this.realFetcher
|
||||
|
@ -84,36 +84,42 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
}
|
||||
|
||||
constructor(
|
||||
fetch: Fetch,
|
||||
fetch: typeof global.fetch,
|
||||
cookieManager: CookieManager,
|
||||
options?: FetcherOptions
|
||||
) {
|
||||
super()
|
||||
this.fetch = wrap(fetch, options);
|
||||
this.realFetcher = this.fetch;
|
||||
this.fetch = wrap(fetch, options)
|
||||
this.realFetcher = this.fetch
|
||||
this.cookieManager = cookieManager
|
||||
}
|
||||
|
||||
public replaceFetcher(fetcher: Fetcher) {
|
||||
this.fetch = fetcher;
|
||||
this.fetch = fetcher
|
||||
}
|
||||
|
||||
async getSchedule(child: EtjanstChild, from: DateTime, to: DateTime): Promise<(CalendarItem & ScheduleItem)[]> {
|
||||
|
||||
async getSchedule(
|
||||
child: EtjanstChild,
|
||||
from: DateTime,
|
||||
to: DateTime
|
||||
): Promise<(CalendarItem & ScheduleItem)[]> {
|
||||
const lessonParams = {
|
||||
forUser: child.id,
|
||||
startDateIso: from.toISODate(),
|
||||
endDateIso: to.toISODate(),
|
||||
}
|
||||
const lessonsResponse = await this.fetch(`lessons-${lessonParams.forUser}`, lessonsUrl(lessonParams))
|
||||
const lessonsResponse = await this.fetch(
|
||||
`lessons-${lessonParams.forUser}`,
|
||||
lessonsUrl(lessonParams)
|
||||
)
|
||||
const lessonsResponseJson: any[] = await lessonsResponse.json()
|
||||
|
||||
return lessonsResponseJson.map(l => {
|
||||
return lessonsResponseJson.map((l) => {
|
||||
const start = DateTime.fromMillis(l.startDate.ts, {
|
||||
zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes)
|
||||
zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes),
|
||||
})
|
||||
const end = DateTime.fromMillis(l.endDate.ts, {
|
||||
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes)
|
||||
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes),
|
||||
})
|
||||
return {
|
||||
id: l.id,
|
||||
|
@ -133,7 +139,6 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
}
|
||||
|
||||
async setSessionCookie(sessionCookie: string): Promise<void> {
|
||||
|
||||
await this.fetch('login-cookie', hjarntorgetUrl, {
|
||||
headers: {
|
||||
cookie: sessionCookie,
|
||||
|
@ -151,7 +156,7 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
}
|
||||
|
||||
async getUser(): Promise<User> {
|
||||
console.log("fetching user")
|
||||
console.log('fetching user')
|
||||
const currentUserResponse = await this.fetch('current-user', currentUserUrl)
|
||||
if (currentUserResponse.status !== 200) {
|
||||
return { isAuthenticated: false }
|
||||
|
@ -165,60 +170,70 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
if (!this.isLoggedIn) {
|
||||
throw new Error('Not logged in...')
|
||||
}
|
||||
console.log("fetching children")
|
||||
console.log('fetching children')
|
||||
|
||||
const myChildrenResponse = await this.fetch('my-children', myChildrenUrl)
|
||||
const myChildrenResponseJson: any[] = await myChildrenResponse.json()
|
||||
|
||||
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)))
|
||||
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[]> {
|
||||
const childEventsAndMembers = await this.getChildEventsWithAssociatedMembers(child)
|
||||
const childEventsAndMembers =
|
||||
await this.getChildEventsWithAssociatedMembers(child)
|
||||
|
||||
// This fetches the calendars search page on Hjärntorget.
|
||||
// This fetches the calendars search page on Hjärntorget.
|
||||
// It is used (at least at one school) for homework schedule
|
||||
// The Id for the "event" that the calendar belongs to is not the same as the ones
|
||||
// The Id for the "event" that the calendar belongs to is not the same as the ones
|
||||
// fetched using the API... So we match them by name :/
|
||||
const calendarsResponse = await this.fetch('calendars', calendarsUrl)
|
||||
const calendarsResponseText = await calendarsResponse.text()
|
||||
const calendarsDoc = html.parse(decode(calendarsResponseText))
|
||||
const calendarCheckboxes = Array.from(calendarsDoc.querySelectorAll('.calendarPageContainer input.checkbox'))
|
||||
const calendarCheckboxes = Array.from(
|
||||
calendarsDoc.querySelectorAll('.calendarPageContainer input.checkbox')
|
||||
)
|
||||
|
||||
let calendarItems: CalendarItem[] = []
|
||||
for (let i = 0; i < calendarCheckboxes.length; i++) {
|
||||
const calendarId = calendarCheckboxes[i].getAttribute('value') || ""
|
||||
const calendarId = calendarCheckboxes[i].getAttribute('value') || ''
|
||||
|
||||
const today = DateTime.fromJSDate(new Date())
|
||||
const start = today.toISODate()
|
||||
const end = today.plus({ days: 30 }).toISODate()
|
||||
const calendarResponse = await this.fetch(`calendar-${calendarId}`, calendarEventUrl(calendarId, start, end))
|
||||
const calendarResponse = await this.fetch(
|
||||
`calendar-${calendarId}`,
|
||||
calendarEventUrl(calendarId, start, end)
|
||||
)
|
||||
const calendarResponseText = await calendarResponse.text()
|
||||
const calendarDoc = html.parse(decode(calendarResponseText))
|
||||
|
||||
const calendarRows = Array.from(calendarDoc.querySelectorAll('.default-table tr'))
|
||||
const calendarRows = Array.from(
|
||||
calendarDoc.querySelectorAll('.default-table tr')
|
||||
)
|
||||
if (!calendarRows.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
calendarRows.shift()
|
||||
const eventName = calendarRows.shift()?.textContent
|
||||
if (childEventsAndMembers.some(e => e.name === eventName)) {
|
||||
|
||||
if (childEventsAndMembers.some((e) => e.name === eventName)) {
|
||||
const items: CalendarItem[] = calendarRows.map(parseCalendarItem)
|
||||
|
||||
calendarItems = calendarItems.concat(items)
|
||||
}
|
||||
}
|
||||
|
||||
return calendarItems;
|
||||
return calendarItems
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
@ -239,11 +254,11 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
const infoResponse = await this.fetch('info', infoUrl)
|
||||
const infoResponseJson: any[] = await infoResponse.json()
|
||||
// TODO: Filter out read messages?
|
||||
return infoResponseJson.map(i => {
|
||||
const body = html.parse(decode(i.body || ""))
|
||||
return infoResponseJson.map((i) => {
|
||||
const body = html.parse(decode(i.body || ''))
|
||||
const bodyText = toMarkdown(i.body)
|
||||
|
||||
const introText = body.innerText || ""
|
||||
const introText = body.innerText || ''
|
||||
const publishedDate = new Date(i.created.ts)
|
||||
|
||||
return {
|
||||
|
@ -254,13 +269,12 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
body: bodyText,
|
||||
published: publishedDate.toISOString(),
|
||||
modified: publishedDate.toISOString(),
|
||||
fullImageUrl: i.creator && fullImageUrl(i.creator.imagePath)
|
||||
fullImageUrl: i.creator && fullImageUrl(i.creator.imagePath),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async getNewsDetails(_child: EtjanstChild, item: NewsItem): Promise<any> {
|
||||
|
||||
await this.fetch('infoSetReadUrl', infoSetReadUrl(item), {
|
||||
method: 'POST',
|
||||
})
|
||||
|
@ -278,41 +292,73 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
}
|
||||
|
||||
async getChildEventsWithAssociatedMembers(child: EtjanstChild) {
|
||||
const hjarntorgetEventsResponse = await this.fetch('events', hjarntorgetEventsUrl)
|
||||
const hjarntorgetEventsResponseJson: any[] = await hjarntorgetEventsResponse.json()
|
||||
const membersInEvents = await Promise.all(hjarntorgetEventsResponseJson.filter(e => e.state === 'ONGOING')
|
||||
.map(async e => {
|
||||
const eventId = e.id as number
|
||||
const hjarntorgetEventsResponse = await this.fetch(
|
||||
'events',
|
||||
hjarntorgetEventsUrl
|
||||
)
|
||||
const hjarntorgetEventsResponseJson: any[] =
|
||||
await hjarntorgetEventsResponse.json()
|
||||
const membersInEvents = await Promise.all(
|
||||
hjarntorgetEventsResponseJson
|
||||
.filter((e) => e.state === 'ONGOING')
|
||||
.map(async (e) => {
|
||||
const eventId = e.id as number
|
||||
|
||||
const rolesInEvenResponse = await this.fetch(`roles-in-event-${eventId}`, rolesInEventUrl(eventId))
|
||||
const rolesInEvenResponseJson: any[] = await rolesInEvenResponse.json()
|
||||
const rolesInEvenResponse = await this.fetch(
|
||||
`roles-in-event-${eventId}`,
|
||||
rolesInEventUrl(eventId)
|
||||
)
|
||||
const rolesInEvenResponseJson: any[] =
|
||||
await rolesInEvenResponse.json()
|
||||
|
||||
const eventMembers = await Promise.all(rolesInEvenResponseJson.map(async r => {
|
||||
const roleId = r.id
|
||||
const membersWithRoleResponse = await this.fetch(`event-role-members-${eventId}-${roleId}`, membersWithRoleUrl(eventId, roleId))
|
||||
const membersWithRoleResponseJson: any[] = await membersWithRoleResponse.json()
|
||||
return membersWithRoleResponseJson
|
||||
}))
|
||||
return { eventId, name: e.name as string, eventMembers: ([] as any[]).concat(...eventMembers) }
|
||||
}))
|
||||
return membersInEvents
|
||||
.filter(e => e.eventMembers.find(p => p.id === child.id))
|
||||
const eventMembers = await Promise.all(
|
||||
rolesInEvenResponseJson.map(async (r) => {
|
||||
const roleId = r.id
|
||||
const membersWithRoleResponse = await this.fetch(
|
||||
`event-role-members-${eventId}-${roleId}`,
|
||||
membersWithRoleUrl(eventId, roleId)
|
||||
)
|
||||
const membersWithRoleResponseJson: any[] =
|
||||
await membersWithRoleResponse.json()
|
||||
return membersWithRoleResponseJson
|
||||
})
|
||||
)
|
||||
return {
|
||||
eventId,
|
||||
name: e.name as string,
|
||||
eventMembers: ([] as any[]).concat(...eventMembers),
|
||||
}
|
||||
})
|
||||
)
|
||||
return membersInEvents.filter((e) =>
|
||||
e.eventMembers.find((p) => p.id === child.id)
|
||||
)
|
||||
}
|
||||
|
||||
async getNotifications(child: EtjanstChild): Promise<Notification[]> {
|
||||
const childEventsAndMembers =
|
||||
await this.getChildEventsWithAssociatedMembers(child)
|
||||
const membersInChildsEvents = childEventsAndMembers.reduce(
|
||||
(acc, e) => acc.concat(e.eventMembers),
|
||||
[] as any[]
|
||||
)
|
||||
|
||||
const childEventsAndMembers = await this.getChildEventsWithAssociatedMembers(child)
|
||||
const membersInChildsEvents = childEventsAndMembers.reduce((acc, e) => acc.concat(e.eventMembers), ([] as any[]))
|
||||
|
||||
const wallMessagesResponse = await this.fetch('wall-events', wallMessagesUrl)
|
||||
const wallMessagesResponse = await this.fetch(
|
||||
'wall-events',
|
||||
wallMessagesUrl
|
||||
)
|
||||
const wallMessagesResponseJson: any[] = await wallMessagesResponse.json()
|
||||
return wallMessagesResponseJson.filter(message =>
|
||||
membersInChildsEvents.find(member => member.id === message.creator.id))
|
||||
.map(message => {
|
||||
return wallMessagesResponseJson
|
||||
.filter((message) =>
|
||||
membersInChildsEvents.find((member) => member.id === message.creator.id)
|
||||
)
|
||||
.map((message) => {
|
||||
const createdDate = new Date(message.created.ts)
|
||||
return {
|
||||
id: message.id,
|
||||
sender: message.creator && `${message.creator.firstName} ${message.creator.lastName}`,
|
||||
sender:
|
||||
message.creator &&
|
||||
`${message.creator.firstName} ${message.creator.lastName}`,
|
||||
dateCreated: createdDate.toISOString(),
|
||||
message: message.body,
|
||||
url: message.url,
|
||||
|
@ -323,7 +369,7 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
})
|
||||
}
|
||||
|
||||
async getSkola24Children(): Promise<(Skola24Child)[]> {
|
||||
async getSkola24Children(): Promise<Skola24Child[]> {
|
||||
if (!this.isLoggedIn) {
|
||||
throw new Error('Not logged in...')
|
||||
}
|
||||
|
@ -331,8 +377,12 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async getTimetable(child: Skola24Child, week: number, year: number, _lang: string): Promise<TimetableEntry[]> {
|
||||
|
||||
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 })
|
||||
|
||||
|
@ -341,15 +391,18 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
startDateIso: startDate.toISODate(),
|
||||
endDateIso: endDate.toISODate(),
|
||||
}
|
||||
const lessonsResponse = await this.fetch(`lessons-${lessonParams.forUser}`, lessonsUrl(lessonParams))
|
||||
const lessonsResponse = await this.fetch(
|
||||
`lessons-${lessonParams.forUser}`,
|
||||
lessonsUrl(lessonParams)
|
||||
)
|
||||
const lessonsResponseJson: any[] = await lessonsResponse.json()
|
||||
|
||||
return lessonsResponseJson.map(l => {
|
||||
return lessonsResponseJson.map((l) => {
|
||||
const start = DateTime.fromMillis(l.startDate.ts, {
|
||||
zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes)
|
||||
zone: FixedOffsetZone.instance(l.startDate.timezoneOffsetMinutes),
|
||||
})
|
||||
const end = DateTime.fromMillis(l.endDate.ts, {
|
||||
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes)
|
||||
zone: FixedOffsetZone.instance(l.endDate.timezoneOffsetMinutes),
|
||||
})
|
||||
return {
|
||||
id: l.id,
|
||||
|
@ -363,7 +416,6 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
dateEnd: start.toISODate(),
|
||||
} as TimetableEntry
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
|
@ -375,55 +427,77 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
|
||||
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()
|
||||
if (personalNumber !== undefined && personalNumber.endsWith('1212121212'))
|
||||
return this.fakeMode()
|
||||
|
||||
this.isFake = false
|
||||
|
||||
console.log("initiating login to hjarntorget")
|
||||
const beginLoginRedirectResponse = await this.fetch('begin-login', beginLoginUrl, {
|
||||
redirect: 'follow'
|
||||
})
|
||||
console.log('initiating login to hjarntorget')
|
||||
const beginLoginRedirectResponse = await this.fetch(
|
||||
'begin-login',
|
||||
beginLoginUrl,
|
||||
{
|
||||
redirect: 'follow',
|
||||
}
|
||||
)
|
||||
|
||||
console.log("prepping??? shibboleth")
|
||||
const shibbolethLoginResponse = await this.fetch('init-shibboleth-login', shibbolethLoginUrl(shibbolethLoginUrlBase((beginLoginRedirectResponse as any).url)), {
|
||||
redirect: 'follow'
|
||||
})
|
||||
console.log('prepping??? shibboleth')
|
||||
const shibbolethLoginResponse = await this.fetch(
|
||||
'init-shibboleth-login',
|
||||
shibbolethLoginUrl(
|
||||
shibbolethLoginUrlBase((beginLoginRedirectResponse as any).url)
|
||||
),
|
||||
{
|
||||
redirect: 'follow',
|
||||
}
|
||||
)
|
||||
|
||||
const shibbolethRedirectUrl = (shibbolethLoginResponse as any).url
|
||||
console.log("initiating bankid...")
|
||||
const initBankIdResponse = await this.fetch('init-bankId', initBankIdUrl(shibbolethRedirectUrl), {
|
||||
redirect: 'follow'
|
||||
})
|
||||
console.log('initiating bankid...')
|
||||
const initBankIdResponse = await this.fetch(
|
||||
'init-bankId',
|
||||
initBankIdUrl(shibbolethRedirectUrl),
|
||||
{
|
||||
redirect: 'follow',
|
||||
}
|
||||
)
|
||||
|
||||
const initBankIdResponseText = await initBankIdResponse.text()
|
||||
const mvghostRequestBody = extractMvghostRequestBody(initBankIdResponseText)
|
||||
|
||||
console.log("picking auth server???")
|
||||
let mvghostResponse = await this.fetch('pick-mvghost', mvghostUrl, {
|
||||
console.log('picking auth server???')
|
||||
const mvghostResponse = await this.fetch('pick-mvghost', mvghostUrl, {
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
body: mvghostRequestBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
})
|
||||
|
||||
console.log("start bankid sign in")
|
||||
console.log('start bankid sign in')
|
||||
// We may get redirected to some other subdomain i.e. not 'm00-mg-local':
|
||||
// https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/other
|
||||
|
||||
const ssnBody = new URLSearchParams({ ssn: personalNumber }).toString()
|
||||
const beginBankIdResponse = await this.fetch('start-bankId', beginBankIdUrl((mvghostResponse as any).url), {
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
body: ssnBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
|
||||
console.log("start polling")
|
||||
const statusChecker = checkStatus(this.fetch, verifyUrlBase((beginBankIdResponse as any).url))
|
||||
const ssnBody = new URLSearchParams({ ssn: personalNumber }).toString()
|
||||
const beginBankIdResponse = await this.fetch(
|
||||
'start-bankId',
|
||||
beginBankIdUrl((mvghostResponse as any).url),
|
||||
{
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
body: ssnBody,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
console.log('start polling')
|
||||
const statusChecker = checkStatus(
|
||||
this.fetch,
|
||||
verifyUrlBase((beginBankIdResponse as any).url)
|
||||
)
|
||||
|
||||
statusChecker.on('OK', async () => {
|
||||
// setting these similar to how the sthlm api does it
|
||||
|
@ -435,7 +509,7 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
statusChecker.on('ERROR', () => {
|
||||
this.personalNumber = undefined
|
||||
})
|
||||
|
||||
|
||||
return statusChecker
|
||||
}
|
||||
|
||||
|
@ -451,4 +525,4 @@ export class ApiHjarntorget extends EventEmitter implements Api {
|
|||
emitter.token = 'fake'
|
||||
return emitter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { ApiHjarntorget } from './apiHjarntorget'
|
||||
import { Api, FetcherOptions, Fetch, RNCookieManager,
|
||||
import {
|
||||
Api,
|
||||
FetcherOptions,
|
||||
RNCookieManager,
|
||||
ToughCookieJar,
|
||||
wrapReactNativeCookieManager,
|
||||
wrapToughCookie } from '@skolplattformen/api'
|
||||
wrapToughCookie,
|
||||
} from '@skolplattformen/api'
|
||||
import { ApiHjarntorget } from './apiHjarntorget'
|
||||
export { features } from './features'
|
||||
|
||||
const init = (
|
||||
fetchImpl: Fetch,
|
||||
fetchImpl: typeof fetch,
|
||||
cookieManagerImpl: RNCookieManager | ToughCookieJar,
|
||||
options?: FetcherOptions
|
||||
): Api => {
|
||||
|
|
|
@ -1,98 +1,122 @@
|
|||
import { Fetcher, LoginStatusChecker } from '@skolplattformen/api'
|
||||
import { EventEmitter } from 'events'
|
||||
import { LoginStatusChecker, Fetcher} from '@skolplattformen/api'
|
||||
import {
|
||||
extractAuthGbgLoginRequestBody,
|
||||
extractHjarntorgetSAMLLogin
|
||||
import {
|
||||
extractAuthGbgLoginRequestBody,
|
||||
extractHjarntorgetSAMLLogin,
|
||||
} from './parse/parsers'
|
||||
import { authGbgLoginUrl, hjarntorgetSAMLLoginUrl, pollStatusUrl } from './routes'
|
||||
import {
|
||||
authGbgLoginUrl,
|
||||
hjarntorgetSAMLLoginUrl,
|
||||
pollStatusUrl,
|
||||
} from './routes'
|
||||
|
||||
export class HjarntorgetChecker extends EventEmitter {
|
||||
private fetcher: Fetcher
|
||||
|
||||
private fetcher: Fetcher
|
||||
private basePollingUrl: string
|
||||
|
||||
private basePollingUrl: string
|
||||
public token: string
|
||||
|
||||
public token: string
|
||||
private cancelled = false
|
||||
|
||||
private cancelled: boolean = false
|
||||
constructor(fetcher: Fetcher, basePollingUrl: string) {
|
||||
super()
|
||||
this.token = '' // not used, but needed for compatability with the LoginStatusChecker
|
||||
this.fetcher = fetcher
|
||||
this.basePollingUrl = basePollingUrl
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
async check(): Promise<void> {
|
||||
try {
|
||||
console.log("polling bankid signature")
|
||||
// https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/pollstatus
|
||||
|
||||
const pollStatusResponse = await this.fetcher('poll-bankid-status', pollStatusUrl(this.basePollingUrl))
|
||||
console.log("poll-bankid-status")
|
||||
const pollStatusResponseJson = await pollStatusResponse.json()
|
||||
const pollStatusResponse = await this.fetcher(
|
||||
'poll-bankid-status',
|
||||
pollStatusUrl(this.basePollingUrl)
|
||||
)
|
||||
console.log('poll-bankid-status')
|
||||
const pollStatusResponseJson = await pollStatusResponse.json()
|
||||
|
||||
const keepPolling = pollStatusResponseJson.infotext !== ''
|
||||
const isError = pollStatusResponseJson.location.indexOf('error') >= 0
|
||||
if (!keepPolling && !isError) {
|
||||
console.log("bankid successfull! follow to location...")
|
||||
// follow response location to get back to auth.goteborg.se
|
||||
// r.location is something like:
|
||||
// 'https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/signature'
|
||||
const signatureResponse = await this.fetcher('confirm-signature-redirect', pollStatusResponseJson.location, {
|
||||
redirect: "follow"
|
||||
})
|
||||
if(!signatureResponse.ok) {
|
||||
throw new Error("Bad signature response")
|
||||
}
|
||||
const signatureResponseText = await signatureResponse.text()
|
||||
const authGbgLoginBody = extractAuthGbgLoginRequestBody(signatureResponseText)
|
||||
|
||||
console.log("authGbg saml login")
|
||||
const authGbgLoginResponse = await this.fetcher('authgbg-saml-login', authGbgLoginUrl, {
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: authGbgLoginBody
|
||||
})
|
||||
if(!authGbgLoginResponse.ok) {
|
||||
throw new Error("Bad augGbgLogin response")
|
||||
}
|
||||
const authGbgLoginResponseText = await authGbgLoginResponse.text()
|
||||
const hjarntorgetSAMLLoginBody = extractHjarntorgetSAMLLogin(authGbgLoginResponseText)
|
||||
|
||||
console.log("hjarntorget saml login")
|
||||
const hjarntorgetSAMLLoginResponse = await this.fetcher('hjarntorget-saml-login', hjarntorgetSAMLLoginUrl, {
|
||||
method: 'POST',
|
||||
redirect: 'follow',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: hjarntorgetSAMLLoginBody,
|
||||
})
|
||||
if(!hjarntorgetSAMLLoginResponse.ok) {
|
||||
throw new Error("Bad hjarntorgetSAMLLogin response")
|
||||
}
|
||||
// TODO: add more checks above between calls to see if everything is actually 'OK'...
|
||||
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')
|
||||
const keepPolling = pollStatusResponseJson.infotext !== ''
|
||||
const isError = pollStatusResponseJson.location.indexOf('error') >= 0
|
||||
if (!keepPolling && !isError) {
|
||||
console.log('bankid successfull! follow to location...')
|
||||
// follow response location to get back to auth.goteborg.se
|
||||
// r.location is something like:
|
||||
// 'https://mNN-mg-local.idp.funktionstjanster.se/mg-local/auth/ccp11/grp/signature'
|
||||
const signatureResponse = await this.fetcher(
|
||||
'confirm-signature-redirect',
|
||||
pollStatusResponseJson.location,
|
||||
{
|
||||
redirect: 'follow',
|
||||
}
|
||||
)
|
||||
if (!signatureResponse.ok) {
|
||||
throw new Error('Bad signature response')
|
||||
}
|
||||
}
|
||||
const signatureResponseText = await signatureResponse.text()
|
||||
const authGbgLoginBody = extractAuthGbgLoginRequestBody(
|
||||
signatureResponseText
|
||||
)
|
||||
|
||||
async cancel(): Promise<void> {
|
||||
this.cancelled = true
|
||||
console.log('authGbg saml login')
|
||||
const authGbgLoginResponse = await this.fetcher(
|
||||
'authgbg-saml-login',
|
||||
authGbgLoginUrl,
|
||||
{
|
||||
redirect: 'follow',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: authGbgLoginBody,
|
||||
}
|
||||
)
|
||||
if (!authGbgLoginResponse.ok) {
|
||||
throw new Error('Bad augGbgLogin response')
|
||||
}
|
||||
const authGbgLoginResponseText = await authGbgLoginResponse.text()
|
||||
const hjarntorgetSAMLLoginBody = extractHjarntorgetSAMLLogin(
|
||||
authGbgLoginResponseText
|
||||
)
|
||||
|
||||
console.log('hjarntorget saml login')
|
||||
const hjarntorgetSAMLLoginResponse = await this.fetcher(
|
||||
'hjarntorget-saml-login',
|
||||
hjarntorgetSAMLLoginUrl,
|
||||
{
|
||||
method: 'POST',
|
||||
redirect: 'follow',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: hjarntorgetSAMLLoginBody,
|
||||
}
|
||||
)
|
||||
if (!hjarntorgetSAMLLoginResponse.ok) {
|
||||
throw new Error('Bad hjarntorgetSAMLLogin response')
|
||||
}
|
||||
// TODO: add more checks above between calls to see if everything is actually 'OK'...
|
||||
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 HjarntorgetChecker(fetch, basePollingUrl)
|
||||
export const checkStatus = (
|
||||
fetch: Fetcher,
|
||||
basePollingUrl: string
|
||||
): LoginStatusChecker => new HjarntorgetChecker(fetch, basePollingUrl)
|
||||
|
|
|
@ -1,32 +1,33 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { decode } from 'he'
|
||||
import { DateTime } from 'luxon'
|
||||
import * as html from 'node-html-parser'
|
||||
import { LoginStatusChecker, FetcherOptions, Fetcher, wrap } from '@skolplattformen/api'
|
||||
import {
|
||||
Api,
|
||||
AuthTicket,
|
||||
CalendarItem,
|
||||
Classmate,
|
||||
CookieManager,
|
||||
EtjanstChild,
|
||||
Fetch,
|
||||
Fetcher,
|
||||
FetcherOptions,
|
||||
LoginStatusChecker,
|
||||
MenuItem,
|
||||
NewsItem,
|
||||
Notification,
|
||||
RequestInit,
|
||||
Response,
|
||||
ScheduleItem,
|
||||
Skola24Child,
|
||||
SSOSystem,
|
||||
TimetableEntry,
|
||||
User,
|
||||
Response
|
||||
wrap,
|
||||
} from '@skolplattformen/api'
|
||||
import * as routes from './routes'
|
||||
import * as parse from './parse/index'
|
||||
import { Language } from '@skolplattformen/curriculum'
|
||||
import { EventEmitter } from 'events'
|
||||
import { decode } from 'he'
|
||||
import { DateTime } from 'luxon'
|
||||
import * as html from 'node-html-parser'
|
||||
import * as fake from './fakeData'
|
||||
import { checkStatus } from './loginStatusChecker'
|
||||
import { Api } from '@skolplattformen/api'
|
||||
import { Language } from '@skolplattformen/curriculum'
|
||||
import * as parse from './parse/index'
|
||||
import * as routes from './routes'
|
||||
|
||||
const fakeResponse = <T>(data: T): Promise<T> =>
|
||||
new Promise((res) => setTimeout(() => res(data), 200 + Math.random() * 800))
|
||||
|
@ -66,7 +67,7 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
|
|||
private authorizedSystems: SSOSystems = {}
|
||||
|
||||
constructor(
|
||||
fetch: Fetch,
|
||||
fetch: typeof global.fetch,
|
||||
cookieManager: CookieManager,
|
||||
options?: FetcherOptions
|
||||
) {
|
||||
|
@ -286,10 +287,17 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
private CheckResponseForCorrectChildStatus(response: Response, child: EtjanstChild) {
|
||||
const setCookieResp = response.headers.get("Set-Cookie")
|
||||
private CheckResponseForCorrectChildStatus(
|
||||
response: Response,
|
||||
child: EtjanstChild
|
||||
) {
|
||||
const setCookieResp = response.headers.get('Set-Cookie')
|
||||
|
||||
if (child.status !== 'FS' && setCookieResp && setCookieResp.includes("Status=FS")) {
|
||||
if (
|
||||
child.status !== 'FS' &&
|
||||
setCookieResp &&
|
||||
setCookieResp.includes('Status=FS')
|
||||
) {
|
||||
throw new Error('Wrong child in response')
|
||||
}
|
||||
}
|
||||
|
@ -297,7 +305,7 @@ export class ApiSkolplattformen extends EventEmitter implements Api {
|
|||
public async getNewsDetails(
|
||||
child: EtjanstChild,
|
||||
item: NewsItem
|
||||
): Promise<NewsItem | undefined > {
|
||||
): Promise<NewsItem | undefined> {
|
||||
if (this.isFake) {
|
||||
return fakeResponse(fake.news(child).find((ni) => ni.id === item.id))
|
||||
}
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { ApiSkolplattformen } from './api'
|
||||
import { Api, FetcherOptions, Fetch, RNCookieManager,
|
||||
import {
|
||||
Api,
|
||||
FetcherOptions,
|
||||
RNCookieManager,
|
||||
ToughCookieJar,
|
||||
wrapReactNativeCookieManager,
|
||||
wrapToughCookie } from '@skolplattformen/api'
|
||||
wrapToughCookie,
|
||||
} from '@skolplattformen/api'
|
||||
import { ApiSkolplattformen } from './api'
|
||||
export { features } from './features'
|
||||
|
||||
const init = (
|
||||
fetchImpl: Fetch,
|
||||
fetchImpl: typeof fetch,
|
||||
cookieManagerImpl: RNCookieManager | ToughCookieJar,
|
||||
options?: FetcherOptions
|
||||
): Api => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Fetch, RequestInit, Response } from './types'
|
||||
import { Response } from './types'
|
||||
|
||||
export interface CallInfo extends RequestInit {
|
||||
name: string
|
||||
|
@ -48,7 +48,7 @@ const record = async (
|
|||
}
|
||||
|
||||
export default function wrap(
|
||||
fetch: Fetch,
|
||||
fetch: typeof global.fetch,
|
||||
options: FetcherOptions = {}
|
||||
): Fetcher {
|
||||
return async (
|
||||
|
|
|
@ -20,15 +20,6 @@ export interface CookieManager {
|
|||
removeAllCookies?: () => Promise<void>
|
||||
}
|
||||
|
||||
export interface RequestInit {
|
||||
headers?: any
|
||||
method?: string
|
||||
body?: string
|
||||
/**
|
||||
* Set to `manual` to extract redirect headers, `error` to reject redirect */
|
||||
redirect?: string
|
||||
}
|
||||
|
||||
export interface Headers {
|
||||
get(name: string): string | null
|
||||
}
|
||||
|
@ -42,10 +33,6 @@ export interface Response {
|
|||
json: () => Promise<any>
|
||||
}
|
||||
|
||||
export interface Fetch {
|
||||
(url: string, init?: RequestInit): Promise<Response>
|
||||
}
|
||||
|
||||
export interface AuthTicket {
|
||||
order: string
|
||||
token: string
|
||||
|
|
Loading…
Reference in New Issue