diff --git a/BackendMetadata/exercises.txt b/BackendMetadata/exercises.txt new file mode 100644 index 0000000..9707fdd --- /dev/null +++ b/BackendMetadata/exercises.txt @@ -0,0 +1,6 @@ +GET https://hgbp.de/iserv/exercise.csv +Payload: HEADER(cookie)[IServSession={sessionKey}; IServSAT={satKey};] + +ON_SUCCESS: +STATUS 200 +CSV RESPONSE \ No newline at end of file diff --git a/BackendMetadata/login.txt b/BackendMetadata/login.txt index bf9b173..b9c498a 100644 --- a/BackendMetadata/login.txt +++ b/BackendMetadata/login.txt @@ -2,5 +2,10 @@ POST https://hgbp.de/iserv/auth/login Payload: FORMDATA(_username: STRING, _password: STRING) ON_SUCCESS: -STATUS 302 -SessionCookie: HEADER(set-cookie)[IServAuthSession={key};...] \ No newline at end of file + +FOLLOW REDIRECT +EXTRACT avatar URL +MAKE REQUEST +FOLLOW REDIRECTS + +Tokens: HEADER(Set-Cookie)[IServSAT={sat-Key}; IServSession={sessionKey}] diff --git a/BetterIServ.Backend/BetterIServ.Backend.csproj b/BetterIServ.Backend/BetterIServ.Backend.csproj index c275bf3..b5be73c 100644 --- a/BetterIServ.Backend/BetterIServ.Backend.csproj +++ b/BetterIServ.Backend/BetterIServ.Backend.csproj @@ -9,7 +9,9 @@ + + diff --git a/BetterIServ.Backend/Controllers/AuthController.cs b/BetterIServ.Backend/Controllers/AuthController.cs new file mode 100644 index 0000000..763bf82 --- /dev/null +++ b/BetterIServ.Backend/Controllers/AuthController.cs @@ -0,0 +1,50 @@ +using BetterIServ.Backend.Entities; +using Microsoft.AspNetCore.Mvc; +using PuppeteerSharp; +using Credentials = BetterIServ.Backend.Entities.Credentials; + +namespace BetterIServ.Backend.Controllers; + +[ApiController] +[Route("auth")] +public class AuthController : ControllerBase { + + [HttpPost("login")] + public async Task> GetAuthKeysV2([FromBody] Credentials credentials) { + await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true }); + await using var page = await browser.NewPageAsync(); + await page.GoToAsync($"https://{credentials.Domain}/iserv/auth/login"); + + await page.WaitForSelectorAsync("#form-username > input"); + await page.Keyboard.TypeAsync(credentials.Username); + await page.Keyboard.PressAsync("Tab"); + await page.Keyboard.TypeAsync(credentials.Password); + await page.ClickAsync("body > div > main > div > div.panel-body > form > div.row > div:nth-child(1) > button"); + await Task.Delay(500); + + var authKeys = new AuthKeys(); + var cookies = await page.GetCookiesAsync(); + foreach (var cookie in cookies) { + switch (cookie.Name) { + case "IServSession": + authKeys.Session = cookie.Value; + break; + case "IServSAT": + authKeys.Sat = cookie.Value; + break; + case "IServAuthSID": + authKeys.AuthSid = cookie.Value; + break; + case "IServSATId": + authKeys.SatId = cookie.Value; + break; + case "IServAuthSession": + authKeys.AuthSession = cookie.Value; + break; + } + } + + return authKeys; + } + +} \ No newline at end of file diff --git a/BetterIServ.Backend/Controllers/HelperController.cs b/BetterIServ.Backend/Controllers/HelperController.cs deleted file mode 100644 index 27f70d9..0000000 --- a/BetterIServ.Backend/Controllers/HelperController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net; -using Microsoft.AspNetCore.Mvc; - -namespace BetterIServ.Backend.Controllers; - -[ApiController] -public class HelperController : ControllerBase { - - [HttpPost("/login")] - public async Task> Login([FromForm] string email, [FromForm] string password) { - try { - using var client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, AllowAutoRedirect = false }) { Timeout = TimeSpan.FromSeconds(5) }; - - var split = email.Split("@"); - var username = split[0]; - var domain = split[1]; - - var form = new FormUrlEncodedContent(new[] { - new KeyValuePair("_username", username), - new KeyValuePair("_password", password) - }); - - var request = new HttpRequestMessage { - RequestUri = new Uri($"https://{domain}/iserv/auth/login"), - Method = HttpMethod.Post, - Content = form - }; - - var response = await client.SendAsync(request); - var header = response.Headers.GetValues("Set-Cookie").First(); - var part = header.Split(";")[0]; - - if (!part.Contains("IServAuthSession")) throw new Exception(); - - return Ok(part.Replace("IServAuthSession=", "")); - } - catch (Exception) { - return Unauthorized(); - } - } - -} \ No newline at end of file diff --git a/BetterIServ.Backend/Controllers/UnitsController.cs b/BetterIServ.Backend/Controllers/UnitsController.cs new file mode 100644 index 0000000..a7edd57 --- /dev/null +++ b/BetterIServ.Backend/Controllers/UnitsController.cs @@ -0,0 +1,61 @@ +using System.Text; +using BetterIServ.Backend.Entities; +using HtmlAgilityPack; +using Microsoft.AspNetCore.Mvc; + +namespace BetterIServ.Backend.Controllers; + +[ApiController] +[Route("units")] +public class UnitsController : ControllerBase { + + [HttpGet("substitution")] + public async Task> GetToday([FromQuery] string url) { + var client = new HttpClient(); + var buffer = await client.GetByteArrayAsync(url); + var raw = Encoding.GetEncoding("ISO-8859-1").GetString(buffer); + var html = new HtmlDocument(); + html.LoadHtml(raw); + + var data = new UnitsData { + Notifications = new List(), + Substitutions = new List() + }; + + var info = html.DocumentNode.SelectSingleNode("//body/center[1]/table"); + for (int i = 2; i < info.ChildNodes.Count; i++) { + var notification = info.ChildNodes[i]; + if (notification.ChildNodes.Count == 2) { + data.Notifications.Add( + notification.ChildNodes[0].InnerText + + notification.ChildNodes[1].InnerText + ); + } + else if (notification.ChildNodes.Count == 1) { + data.Notifications.Add(notification.ChildNodes[0].InnerText); + } + } + + var substitutions = html.DocumentNode.SelectNodes("//body/center[1]")[0].ChildNodes[6]; + for (int i = 4; i < substitutions.ChildNodes.Count; i++) { + var node = substitutions.ChildNodes[i]; + if (node.ChildNodes.Count < 9) continue; + + var substitution = new Substitution { + Class = node.ChildNodes[0].InnerText, + Times = node.ChildNodes[1].InnerText.Split(" - ").Select(int.Parse).ToArray(), + Type = node.ChildNodes[2].InnerText, + Representative = node.ChildNodes[3].InnerText, + Lesson = node.ChildNodes[4].InnerText, + Room = node.ChildNodes[6].InnerText, + Teacher = node.ChildNodes[7].InnerText, + Description = node.ChildNodes[9].InnerText + }; + + data.Substitutions.Add(substitution); + } + + return data; + } + +} \ No newline at end of file diff --git a/BetterIServ.Backend/Entities/AuthKeys.cs b/BetterIServ.Backend/Entities/AuthKeys.cs new file mode 100644 index 0000000..6852b9a --- /dev/null +++ b/BetterIServ.Backend/Entities/AuthKeys.cs @@ -0,0 +1,9 @@ +namespace BetterIServ.Backend.Entities; + +public struct AuthKeys { + public string Session { get; set; } + public string Sat { get; set; } + public string AuthSid { get; set; } + public string SatId { get; set; } + public string AuthSession { get; set; } +} \ No newline at end of file diff --git a/BetterIServ.Backend/Entities/Credentials.cs b/BetterIServ.Backend/Entities/Credentials.cs index a561950..e01e215 100644 --- a/BetterIServ.Backend/Entities/Credentials.cs +++ b/BetterIServ.Backend/Entities/Credentials.cs @@ -1,8 +1,7 @@ namespace BetterIServ.Backend.Entities; public class Credentials { - public string? Domain { get; set; } - public string? Username { get; set; } - public string? Password { get; set; } - public string? Token { get; set; } -} \ No newline at end of file + public required string Domain { get; set; } + public required string Username { get; set; } + public required string Password { get; set; } +} diff --git a/BetterIServ.Backend/Entities/MailData.cs b/BetterIServ.Backend/Entities/MailData.cs index e174518..c12cfb9 100644 --- a/BetterIServ.Backend/Entities/MailData.cs +++ b/BetterIServ.Backend/Entities/MailData.cs @@ -2,8 +2,8 @@ public sealed class MailData : Credentials { - public string? MailBody { get; set; } - public string? Receiver { get; set; } - public string? Subject { get; set; } + public required string MailBody { get; set; } + public required string Receiver { get; set; } + public required string Subject { get; set; } } \ No newline at end of file diff --git a/BetterIServ.Backend/Entities/SingleResult.cs b/BetterIServ.Backend/Entities/SingleResult.cs index 8f8a384..591840e 100644 --- a/BetterIServ.Backend/Entities/SingleResult.cs +++ b/BetterIServ.Backend/Entities/SingleResult.cs @@ -1,5 +1,5 @@ namespace BetterIServ.Backend.Entities; -public class SingleResult { +public struct SingleResult { public TValue? Value { get; set; } } \ No newline at end of file diff --git a/BetterIServ.Backend/Entities/Substitution.cs b/BetterIServ.Backend/Entities/Substitution.cs new file mode 100644 index 0000000..e285a39 --- /dev/null +++ b/BetterIServ.Backend/Entities/Substitution.cs @@ -0,0 +1,12 @@ +namespace BetterIServ.Backend.Entities; + +public struct Substitution { + public string Class { get; set; } + public int[] Times { get; set; } + public string Type { get; set; } + public string Representative { get; set; } + public string Lesson { get; set; } + public string Room { get; set; } + public string Teacher { get; set; } + public string Description { get; set; } +} diff --git a/BetterIServ.Backend/Entities/UnitsData.cs b/BetterIServ.Backend/Entities/UnitsData.cs new file mode 100644 index 0000000..2659ecc --- /dev/null +++ b/BetterIServ.Backend/Entities/UnitsData.cs @@ -0,0 +1,6 @@ +namespace BetterIServ.Backend.Entities; + +public struct UnitsData { + public IList Notifications { get; set; } + public IList Substitutions { get; set; } +} \ No newline at end of file diff --git a/BetterIServ.Backend/Program.cs b/BetterIServ.Backend/Program.cs index ee1a01c..552506a 100644 --- a/BetterIServ.Backend/Program.cs +++ b/BetterIServ.Backend/Program.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Cors.Infrastructure; +using PuppeteerSharp; + +await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); var builder = WebApplication.CreateBuilder(args); diff --git a/BetterIServ.Mobile/src/app/api/iserv.service.ts b/BetterIServ.Mobile/src/app/api/iserv.service.ts index 69f0bfa..1b928aa 100644 --- a/BetterIServ.Mobile/src/app/api/iserv.service.ts +++ b/BetterIServ.Mobile/src/app/api/iserv.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import {HttpClient} from "@angular/common/http"; -import {Userdata} from "../entities/userdata"; +import {Userdata, AuthKeys} from "../entities/userdata"; import {firstValueFrom} from "rxjs"; @Injectable({ @@ -20,17 +20,14 @@ export class IServService { public async login(email: string, password: string): Promise { const split = email.split('@'); - this.userdata = {}; - this.userdata.username = split[0]; - this.userdata.domain = split[1]; - this.userdata.password = password; - - const data = new FormData(); - data.append("email", email); - data.append("password", password); + this.userdata = { + username: split[0], + domain: split[1], + password + }; try { - this.userdata.token = await firstValueFrom(this.client.post(`${this.backend}/login`, data, {responseType: "text"})); + await firstValueFrom(this.client.post(this.backend + "/auth/login", this.userdata)); localStorage.setItem("userdata", JSON.stringify(this.userdata)); return true; }catch (error) { @@ -38,4 +35,8 @@ export class IServService { } } + public async getKeys(): Promise { + return await firstValueFrom(this.client.post(this.backend + "/auth/login", this.userdata)); + } + } diff --git a/BetterIServ.Mobile/src/app/api/mail.service.ts b/BetterIServ.Mobile/src/app/api/mail.service.ts index 49e71be..71ae917 100644 --- a/BetterIServ.Mobile/src/app/api/mail.service.ts +++ b/BetterIServ.Mobile/src/app/api/mail.service.ts @@ -37,7 +37,6 @@ export class MailService { domain: this.iserv.userdata.domain, username: this.iserv.userdata.username, password: this.iserv.userdata.password, - token: this.iserv.userdata.token, subject, receiver, mailBody }; await firstValueFrom(this.client.post(this.iserv.backend + "/mail/send", data)); diff --git a/BetterIServ.Mobile/src/app/api/units.service.ts b/BetterIServ.Mobile/src/app/api/units.service.ts new file mode 100644 index 0000000..bbd6e1d --- /dev/null +++ b/BetterIServ.Mobile/src/app/api/units.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {IServService} from "./iserv.service"; +import {UnitsData} from "../entities/substitution"; +import {firstValueFrom} from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class UnitsService { + + private schools: {[domain: string]: {today: string, tomorrow: string}} = { + ["hgbp.de"]: { + today: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/heute/subst_001.htm", + tomorrow: "https://www.humboldt-gymnasium.de/vertretungsplan/PlanINet/morgen/subst_001.htm" + } + } + + constructor(private iserv: IServService, private client: HttpClient) {} + + public async getSubstitutionPlan(date: "today" | "tomorrow"): Promise { + if (this.schools[this.iserv.userdata.domain] == undefined) return undefined; + const url = this.schools[this.iserv.userdata.domain][date]; + return await firstValueFrom(this.client.get(this.iserv.backend + "/units/substitution?url=" + url)); + } + +} diff --git a/BetterIServ.Mobile/src/app/app.routes.ts b/BetterIServ.Mobile/src/app/app.routes.ts index 6c5d835..83cbf65 100644 --- a/BetterIServ.Mobile/src/app/app.routes.ts +++ b/BetterIServ.Mobile/src/app/app.routes.ts @@ -22,4 +22,8 @@ export const routes: Routes = [ path: 'mails', loadComponent: () => import('./pages/mails/mails.page').then( m => m.MailsPage) }, + { + path: 'substitution', + loadComponent: () => import('./pages/substitution/substitution.page').then( m => m.SubstitutionPage) + }, ]; diff --git a/BetterIServ.Mobile/src/app/entities/substitution.ts b/BetterIServ.Mobile/src/app/entities/substitution.ts new file mode 100644 index 0000000..4360b79 --- /dev/null +++ b/BetterIServ.Mobile/src/app/entities/substitution.ts @@ -0,0 +1,15 @@ +export interface Substitution { + class: string; + times: number[]; + type: string; + representative: string; + lesson: string; + room: string; + teacher: string; + description: string; +} + +export interface UnitsData { + notifications: string[]; + substitutions: Substitution[]; +} diff --git a/BetterIServ.Mobile/src/app/entities/userdata.ts b/BetterIServ.Mobile/src/app/entities/userdata.ts index 3c908a4..78853ea 100644 --- a/BetterIServ.Mobile/src/app/entities/userdata.ts +++ b/BetterIServ.Mobile/src/app/entities/userdata.ts @@ -1,6 +1,13 @@ export interface Userdata { - domain?: string, - username?: string, - password?: string, - token?: string + domain: string; + username: string; + password: string; +} + +export interface AuthKeys { + session: string; + sat: string; + authSid: string; + satId: string; + authSession: string; } diff --git a/BetterIServ.Mobile/src/app/pages/files/files.page.scss b/BetterIServ.Mobile/src/app/pages/files/files.page.scss index ed5dd56..d3f6835 100644 --- a/BetterIServ.Mobile/src/app/pages/files/files.page.scss +++ b/BetterIServ.Mobile/src/app/pages/files/files.page.scss @@ -2,7 +2,3 @@ ion-label { margin-left: 10px; cursor: pointer; } - -.active { - color: var(--ion-color-primary); -} diff --git a/BetterIServ.Mobile/src/app/pages/login/login.page.ts b/BetterIServ.Mobile/src/app/pages/login/login.page.ts index 7178846..68b8c8f 100644 --- a/BetterIServ.Mobile/src/app/pages/login/login.page.ts +++ b/BetterIServ.Mobile/src/app/pages/login/login.page.ts @@ -33,8 +33,6 @@ export class LoginPage implements OnInit { await alert.present(); } - - console.log(this.iservApi.userdata); } } diff --git a/BetterIServ.Mobile/src/app/pages/mails/mails.page.scss b/BetterIServ.Mobile/src/app/pages/mails/mails.page.scss index 5fb1bd7..27d19f0 100644 --- a/BetterIServ.Mobile/src/app/pages/mails/mails.page.scss +++ b/BetterIServ.Mobile/src/app/pages/mails/mails.page.scss @@ -1,7 +1,3 @@ -ion-select { - padding-inline: 15px; -} - .mail > div { display: flex; flex-direction: column; diff --git a/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.html b/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.html new file mode 100644 index 0000000..42f6107 --- /dev/null +++ b/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.html @@ -0,0 +1,73 @@ + + + + + + + + + Heute + + + Morgen + + + + + + + + + Vertretungsplan + + + + + + + +
+ + + + + +
+ +
+ + + {{subs.times.join(" - ")}} +
+ {{subs.type}} + +
+
+
+
+
+ + + + + + + + + Vertretungen + + + + + + Nachrichten + + + + + + diff --git a/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.scss b/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.scss new file mode 100644 index 0000000..9930ba4 --- /dev/null +++ b/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.scss @@ -0,0 +1,48 @@ +.subs { + ion-card-content { + display: flex; + gap: 15px; + color: #FFF; + + .times { + font-size: 25px; + line-height: 25px; + align-self: center; + } + + div { + margin-left: auto; + display: flex; + flex-direction: column; + align-items: end; + + .type { + font-size: 18px; + } + } + } + + &.Raumtausch { + --background: var(--ion-color-success-shade); + } + + &.bittebeachten { + --background: var(--ion-color-warning-shade); + } + + &.Vertretung { + --background: var(--ion-color-primary-shade); + } + + &.Entfall, &.Stillarbeit { + --background: var(--ion-color-danger-shade); + } + + &.Verlegung { + --background: var(--ion-color-secondary-shade); + } + + &.hide { + display: none; + } +} diff --git a/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.ts b/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.ts new file mode 100644 index 0000000..1446b78 --- /dev/null +++ b/BetterIServ.Mobile/src/app/pages/substitution/substitution.page.ts @@ -0,0 +1,51 @@ +import { Component, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import {UnitsService} from "../../api/units.service"; +import {UnitsData} from "../../entities/substitution"; + +@Component({ + selector: 'app-substitution', + templateUrl: './substitution.page.html', + styleUrls: ['./substitution.page.scss'], + standalone: true, + imports: [IonicModule, CommonModule, FormsModule] +}) +export class SubstitutionPage implements OnInit { + + public data: UnitsData; + public showNews: boolean = false; + public currentClass: string; + + constructor(private units: UnitsService) { + this.currentClass = localStorage.getItem("class"); + } + + async ngOnInit() { + this.data = await this.units.getSubstitutionPlan("today"); + } + + public async changeDate(date: string) { + this.data = await this.units.getSubstitutionPlan(date as "today" | "tomorrow"); + } + + public getDistinctClasses(): string[] { + const classes: string[] = []; + if (this.data == undefined) return []; + + for (let subs of this.data.substitutions) { + if (classes.indexOf(subs.class) == -1) { + classes.push(subs.class) + } + } + + return classes; + } + + public changeClass(className: string) { + this.currentClass = className; + localStorage.setItem("class", className); + } + +} diff --git a/BetterIServ.Mobile/src/global.scss b/BetterIServ.Mobile/src/global.scss index 1e0fb47..3cc54b2 100644 --- a/BetterIServ.Mobile/src/global.scss +++ b/BetterIServ.Mobile/src/global.scss @@ -32,3 +32,11 @@ ion-menu-button { .pointer { cursor: pointer; } + +ion-select { + padding-inline: 15px; +} + +.active { + color: var(--ion-color-primary); +}