Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

i871: F5 in WTI causes logout #1021

Merged
merged 86 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from 81 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
5338cf4
i186: removed "defaultProject" from angular.json;
Oct 28, 2023
a65cd49
i186: removed unsupported option "es5BrowserSupport" from angular.json:
Oct 28, 2023
de5c111
i186: updated WTI-UI package.json to Angular 16 and its dependencies.
Oct 28, 2023
3e23f02
i186: additional package.json version updates: angular/material & /cdk.
Oct 28, 2023
7630ef3
i186: updated app.module for new angular/material subclass imports.
Oct 29, 2023
fa796fc
i186: removed "entryComponents" declarations; these were...
Oct 29, 2023
74e3b98
i833: update .github/workflows/ci.yml to require node v16.20.x
Oct 29, 2023
e742b8d
i833: update github "checkout" action from V2 to V4.
Oct 29, 2023
8cc4f89
i833: update package.json to specify core-js v3.33.1 over deprecated 2.6
Oct 29, 2023
ff860b7
Merge pull request #834 from clevengr/i_833_Upgrade_WTI_Angular_Compo…
lane55 Nov 4, 2023
b8102cc
i871: added global constants file to WTI-UI project.
Nov 28, 2023
c8b409b
i871: update app.component in prep for supporting F5 redirection:
Nov 28, 2023
60b60df
i871: attempt to enable browser console route tracing (doesn't work?)
Nov 28, 2023
860433f
i871: save "runs page" to sessionStorage on view startup.
Nov 28, 2023
18e2af7
i871: added 'title' elements to Angular routing paths.
clevengr Nov 29, 2023
c4bc6c3
i871: subscribe to router events in AppComponent constructor for debug.
Dec 5, 2023
179d976
i871: update AuthService.isLoggedIn() to also check for "current page".
Dec 5, 2023
bb45d5b
i871: update WTI-UI constants.ts with keys for every UI page.
clevengr Oct 19, 2024
6e1723d
i871: make clars and scorebd pages update localStorage "curPage".
clevengr Oct 19, 2024
51a0bb3
i871: make options page update localStorage curPage
clevengr Oct 19, 2024
8dfd00b
i871: refactor loadEnvironment(); call on Refresh as well as initial.
clevengr Oct 19, 2024
cde0e9c
i871: add console debugging output.
clevengr Oct 19, 2024
fb0eab2
i871: remove RouterModule tracing (previously added for debugging).
clevengr Oct 19, 2024
c9a3689
i871: add debugging console output.
clevengr Oct 19, 2024
cdf459b
i871: add code to save/restore Auth token/username in browser.
clevengr Oct 22, 2024
52db6e2
i871: add ContestService as core module provider.
clevengr Oct 22, 2024
ebb101a
I871: major updates to multiple modules, as follows:
Nov 26, 2024
a46b66e
I871: fixed incorrect parameter names in AuthService.
clevengr Nov 26, 2024
9ef0128
i871: set DEBUG_MODE to true (should be reset to null before release)
clevengr Nov 27, 2024
1d45993
i871: added debug code and documentation comments.
clevengr Nov 27, 2024
71628f6
i871: added debug print statements
clevengr Nov 27, 2024
e570d65
i871: add "build>options" props to force generation of JS->TS sourcemaps
Dec 1, 2024
b577f3d
i871: added debug output to multiple files.
Dec 3, 2024
f848105
i871: added param to @injectable forcing singleton; added debug output
Dec 3, 2024
edd8e77
i871: added no-arg constructor which prints debug output & calls super()
Dec 3, 2024
4e146a6
i871: added debug output
clevengr Dec 6, 2024
fa8d6a8
i871: made abstract IContestService Injectable,providedIn:root. Also,
clevengr Dec 6, 2024
e584e0e
i871: made services Injectable and providedIn:root.
clevengr Dec 6, 2024
b1ec913
i871: removed no-longer-needed import of Injector.
clevengr Dec 6, 2024
455cc8d
i871: injected services via ctor params instead of using an Injector.
clevengr Dec 6, 2024
4cfd9a4
i871: updated debugging output messages to properly reflect code flow.
Dec 6, 2024
867f558
i871: change ctor param from ContestService to IContestService.
Dec 8, 2024
ce6fe5b
i186: updated WTI-UI package.json to Angular 16 and its dependencies.
Oct 28, 2023
7a270b3
i186: additional package.json version updates: angular/material & /cdk.
Oct 28, 2023
5daee21
i871: added global constants file to WTI-UI project.
Nov 28, 2023
4278c04
i871: update app.component in prep for supporting F5 redirection:
Nov 28, 2023
12e95ad
i871: attempt to enable browser console route tracing (doesn't work?)
Nov 28, 2023
9ec3cd7
i871: save "runs page" to sessionStorage on view startup.
Nov 28, 2023
d4fe6ca
i871: added 'title' elements to Angular routing paths.
clevengr Nov 29, 2023
89ff46e
i871: subscribe to router events in AppComponent constructor for debug.
Dec 5, 2023
ed3de67
i871: update AuthService.isLoggedIn() to also check for "current page".
Dec 5, 2023
e96eed2
i871: update WTI-UI constants.ts with keys for every UI page.
clevengr Oct 19, 2024
768d627
i871: make clars and scorebd pages update localStorage "curPage".
clevengr Oct 19, 2024
0decd75
i871: make options page update localStorage curPage
clevengr Oct 19, 2024
9808508
i871: refactor loadEnvironment(); call on Refresh as well as initial.
clevengr Oct 19, 2024
34f87de
i871: add console debugging output.
clevengr Oct 19, 2024
16745c9
i871: remove RouterModule tracing (previously added for debugging).
clevengr Oct 19, 2024
6337158
i871: add debugging console output.
clevengr Oct 19, 2024
b551ca0
i871: add code to save/restore Auth token/username in browser.
clevengr Oct 22, 2024
fea07e7
i871: add ContestService as core module provider.
clevengr Oct 22, 2024
fcafdc4
I871: major updates to multiple modules, as follows:
Nov 26, 2024
468a1e3
I871: fixed incorrect parameter names in AuthService.
clevengr Nov 26, 2024
9c51ec9
i871: set DEBUG_MODE to true (should be reset to null before release)
clevengr Nov 27, 2024
3f9c4d9
i871: added debug code and documentation comments.
clevengr Nov 27, 2024
28459c5
i871: added debug print statements
clevengr Nov 27, 2024
fd2b1b6
i871: add "build>options" props to force generation of JS->TS sourcemaps
Dec 1, 2024
5335d17
i871: added debug output to multiple files.
Dec 3, 2024
81255ec
i871: added param to @injectable forcing singleton; added debug output
Dec 3, 2024
8c91b01
i871: added no-arg constructor which prints debug output & calls super()
Dec 3, 2024
0d3b626
i871: added debug output
clevengr Dec 6, 2024
dbfba5e
i871: made abstract IContestService Injectable,providedIn:root. Also,
clevengr Dec 6, 2024
95dac1c
i871: made services Injectable and providedIn:root.
clevengr Dec 6, 2024
b1519f9
i871: removed no-longer-needed import of Injector.
clevengr Dec 6, 2024
aa235e7
i871: injected services via ctor params instead of using an Injector.
clevengr Dec 6, 2024
cf408cc
i871: updated debugging output messages to properly reflect code flow.
Dec 6, 2024
85bb3f6
i871: change ctor param from ContestService to IContestService.
Dec 8, 2024
8af8601
Merge branch 'i871_F5_in_WTI' of https://github.com/clevengr/pc2v9.gi…
clevengr Dec 10, 2024
572e23a
i871: delete conflict markers overlooked during merge.
clevengr Dec 10, 2024
a8e4d94
i871: use "IWebsocketService" instead of "WebsocketService". Result:
clevengr Dec 10, 2024
4605979
i871: fix typo in console output.
clevengr Dec 10, 2024
ce3a96f
i871: added class-level documentation on code flow.
clevengr Dec 11, 2024
718fd4f
i871: restore OptionsPage values from sessionStorage on F5 restart.
Dec 15, 2024
1641b6a
i871: add constants for Option Details; rename constants for clarity.
Dec 15, 2024
a6a2589
i871: add OptionsPage radiobtn handlers to save state in sessionStorage
Dec 15, 2024
fc15f20
i871: add debug output to UiHelperService constructor.
Dec 15, 2024
9bbe022
i871: save opts in sessionStorage in ngOnInit and in btn handlers; also,
Dec 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion projects/WTI-UI/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
"styles": [
"src/styles/styles.scss"
],
"scripts": []
"scripts": [],
"sourceMap": true,
"optimization": false
},
"configurations": {
"production": {
Expand Down
6 changes: 6 additions & 0 deletions projects/WTI-UI/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,31 @@ import { ScoreboardPageComponent } from './modules/scoreboard/components/scorebo
const routes: Routes = [
{
path: 'login',
title: 'Login to PC2',
component: LoginPageComponent
}, {
path: 'runs',
title: 'Submit Runs',
component: RunsPageComponent,
canActivate: [AuthGuard]
}, {
path: 'options',
title: 'Configure Options',
component: OptionsPageComponent,
canActivate: [AuthGuard]
}, {
path: 'logout',
title: 'Logout of PC2',
component: LogoutComponent,
canActivate: [AuthGuard]
}, {
path: 'clarifications',
title: 'Submit Clarification Request',
component: ClarificationsPageComponent,
canActivate: [AuthGuard]
}, {
path: 'scoreboard',
title: 'View Scoreboard',
component: ScoreboardPageComponent,
canActivate: [AuthGuard]
}, {
Expand Down
241 changes: 227 additions & 14 deletions projects/WTI-UI/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,241 @@
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import * as Constants from 'src/constants';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/modules/core/auth/auth.service' ;
import { IContestService } from 'src/app/modules/core/abstract-services/i-contest.service' ;
import { IWebsocketService } from 'src/app/modules/core/abstract-services/i-websocket.service' ;
import { DEBUG_MODE } from 'src/constants';

/*
This AppComponent class is the main starting point for the WTI-UI Angular Single-Page-Application (SPA).
(The overall SPA starts in main.ts, which invokes app.module.ts, which in turn bootstraps this AppComponent class.)

When app.module.ts invokes this class's constructor, the constructor parameters cause TypeScript to automatically construct
local property variables (objects) of type HttpClient, Router, AuthService, IContestService, and IWebsocketService.
Construcing the AuthService object in turn causes creation of an ITeamsService object.

The AuthService, IContestService, ITeamsService, and IWebsocketService classes are all listed in the "providers" array
of class CoreModule (core.module.ts), which means that CoreModule is responsible for providing those service classes.
All four service classes are marked as "injectable", which means they can be injected into other classes. All four classes
are marked as "providedIn: 'root'" in their "@Injectable" decorator, which means they are all defined as singletons provided
by (injected by) the "root injector"

The latter three classes (IContestService, ITeamsService, and IWebsocketService) are listed in CoreModule with "provide" properties
indicating that they are to be "provided" by corresponding "factory methods" (also in CoreModule). These factory methods choose
between "real" and "mock" service providers depending on the value of an "environment flag" named "useMock" (see the files
under "environments").

Once construction is complete, this AppComponent then starts running at its "ngOnInit()" method.
ngOnInit() checks the browser's "sessionStorage" to see if there is a "current page" recorded there.
If not, AppComponent invokes "loadEnvironment()" to load the SPA configuration from file "assets/appconfig.json".

If an existing "current page" IS found in "sessionStorage", AppComponent interprets it as indicating that a "browser refresh"
(F5) has occurred. In this case, AppComponent performs the same "loadEnvironment() operations, then loads additional state
information (userName and token value) from "sessionStorage" and restores it into the AuthService class.
It then startsa new WebSocket for communication with the WTI Server, checks the ContestService "isContestRunning" value
(using it to trigger updates to things like display of problem names), and finally uses the Router to navigate to the
previous SPA page.

Meanwhile, as part of the AppModule bootstrap process, module "AppRoutingModule" sets up a list of "available routes"
(that is, pages which the SPA knows how to transfer to), with the first (default) route being the LoginPageComponent.
This causes the SPA to display the LoginPageComponent, which builds a "submission form" for the user to enter
team name and password. When this form's "Submit" button is clicked,
the LoginPageComponent.onSubmit() method invokes the AuthService's "login" method, which connects to
the WTI server and logs the user into the PC2 server, then (on successful login) uses the Router's
path list to transfer to the "/runs" page.

*/

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})

export class AppComponent implements OnInit {
configLoaded = false;

constructor(private _httpClient: HttpClient) { }
constructor(private _httpClient: HttpClient,
public router: Router,
private _authService: AuthService,
private _contestService: IContestService,
private _websocketService: IWebsocketService) {
if (DEBUG_MODE) {
console.log("Executing AppComponent constructor...");
}
//this.router.events.subscribe(console.log); //shows router tracing on console
}

ngOnInit(): void {
// Load appconfig.json from assets directory, overwrite environment.ts
// with these values
this._httpClient.get('assets/appconfig.json')
.subscribe((data: any) => {
this.configLoaded = true;
if (!data) { return; }
Object.keys(data).forEach((key: string) => environment[key] = data[key]);
}, (error: any) => {
console.log('could not find appconfig.json in assets directory. using default values!');
this.configLoaded = true;
});
}
}

if (DEBUG_MODE) {
console.log ("Entering AppComponent.ngOnInit()...");
console.log ("...sessionStorage.length = " + sessionStorage.length);
if (sessionStorage.length > 0) {
console.log ("...sessionStorage values:");
for (let i = 0; i < sessionStorage.length; i++) {
let key = sessionStorage.key(i);
let value = sessionStorage.getItem(key);
console.log(" Key: ", key, " Value: ", value);
}
}
}

//check if we're loading for the first time
if (!getCurrentPage()) {
if (DEBUG_MODE) {
console.log ('Starting Single-Page-Application from scratch...');
}
//we have no current page so we must be starting from scratch;
// the following matches the sum total of what ngOnInit() used to do before "F5 handling" was added -- jlc
this.loadEnvironment();

} else {
//there is a current page stored; we must be reloading from (e.g.) an F5 refresh
if (DEBUG_MODE) {
console.log ('Restarting Single-Page-Application after refresh navigation...');
}

//restore former environment
if (DEBUG_MODE) {
console.log ("...Loading environment...") ;
}
this.loadEnvironment();

//The following was initially done by the login-page component's onSubmit() method during login;
// this F5 "restore state" code needs to accomplish the equivalent:
//this._authService.login(loginCreds)
// .pipe(takeUntil(this._unsubscribe))
// .subscribe((result: TeamsLoginResponse) => {
// this._authService.completeLogin(result.teamId, result.teamName); //save 'teamId' returned from HTTP login "token"
// this._websocketService.startWebsocket(); //create a websocket connection to the WTI Server
// this._contestService.getIsContestRunning() //get contest isRunning state and save in ContestService
// .subscribe((val: boolean) => {
// this._contestService.isContestRunning = val;
// this._contestService.contestClock.next();
// });
// }

//restore the connection token and username into the AuthService from browser sessionStorage
let token = getCurrentToken();
let username = getCurrentUserName();
if (DEBUG_MODE) {
if (!!token && !!username) {
console.log("...restoring token '" + token + "' and username '" + username + "' into AuthService..." );
}
}

//check whether we found non-null token and username from sessionStorage
if (! (!!token && !!username) ) {
console.warn ("Attempting to reload after F5 restart but found invalid state: token = ", token, " and username = ", username, " !!") ;
//TODO: what should happen if the above occurs??
} else {
//put the token and username back into AuthService (accomplishing what was originally done
// by AuthService.completeLogin(teamId, teamName); teamId is the "token")
this._authService.token = token;
this._authService.username = username;
}

//re-create websocket connection to the WTI Server
if (DEBUG_MODE) {
console.log ("...calling WebsocketService.startWebsocket()...");
}
this._websocketService.startWebsocket();

//update the "is contest running" value in the IContestService object using the value returned from
// an HTTP call within ContestService.getIsContestRunning()
this._contestService.getIsContestRunning()
.subscribe((val: boolean) => {
if (DEBUG_MODE) {
console.log("AppComponent F5 'restore-state' code: ");
console.log (" Subscription callback from ContestService.getIsContestRunning() returned: ", val);
console.log (" ContestService object has uniqueId", this._contestService.uniqueId);
//JSON.stringify() can't handle recursive objects like Angular Subjects
//console.log (JSON.stringify(this._contestService,null,2)); //obj, replacerFunction, indent
console.log (" and contents:");
console.log (this._contestService);
}
this._contestService.isContestRunning = val;
this._contestService.contestClock.next();
});

// transfer to the (former) "current page".
let page = getCurrentPage();
if (DEBUG_MODE) {
console.log ('...navigating to previous page: ' + page);
}

//navigate to the most recently saved page
// TODO: consider whether using history.pushState()/popState() is a better solution for this...
this.router.navigate([page])
.then(nav => {
if (DEBUG_MODE) {
console.log ("...navigation complete.");
}
}, err => {
console.log("Call to router.navigate([" + page + "]) failed (returned '" + err + "')");
});

}

}

// Load appconfig.json from assets directory, overwrite environment.ts with these values
loadEnvironment(): void {
if (DEBUG_MODE) {
console.log("...loading environment from 'assets/appconfig.json'...");
}
this._httpClient.get('assets/appconfig.json')
.subscribe((data: any) => {
this.configLoaded = true;
if (!data) { return; }
Object.keys(data).forEach((key: string) => environment[key] = data[key]);
}, (error: any) => {
console.log('Could not find appconfig.json in assets directory. using default values!');
this.configLoaded = true;
});
}//end function loadEnvironment()

}//end class AppComponent

//save the current page in sessionStorage so a subsequent F5 (refresh) can return to that page
export function saveCurrentPage(page:string) {
sessionStorage.setItem(Constants.CURRENT_PAGE_KEY, page) ;
}

//return the most recently saved page
export function getCurrentPage() {
return (sessionStorage.getItem(Constants.CURRENT_PAGE_KEY));
}

//clear any record of a "current page"
export function clearCurrentPage() {
sessionStorage.removeItem(Constants.CURRENT_PAGE_KEY);
}

//save the current token in sessionStorage so a subsequent F5 (refresh) can restore it
export function saveCurrentToken(token:string) {
sessionStorage.setItem(Constants.CONNECTION_TOKEN_KEY, token) ;
}

//return the most recently saved token
export function getCurrentToken() {
return (sessionStorage.getItem(Constants.CONNECTION_TOKEN_KEY));
}

//save the current username in sessionStorage so a subsequent F5 (refresh) can restore it
export function saveCurrentUserName(username:string) {
sessionStorage.setItem(Constants.CONNECTION_USERNAME_KEY, username) ;
}

//return the most recently saved username
export function getCurrentUserName() {
return (sessionStorage.getItem(Constants.CONNECTION_USERNAME_KEY));
}

//clear all data in sessionStorage
export function clearSessionStorage() {
sessionStorage.clear();
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { MatDialog } from '@angular/material/dialog';
import { NewClarificationComponent } from '../new-clarification/new-clarification.component';
import { AuthService } from '../../../core/auth/auth.service';
import { AppTitleService } from 'src/app/modules/core/services/app-title.service';
import { saveCurrentPage } from 'src/app/app.component';
import * as Constants from 'src/constants';

@Component({
templateUrl: './clarifications-page.component.html',
Expand All @@ -29,7 +31,10 @@ export class ClarificationsPageComponent implements OnInit, OnDestroy {
ngOnInit(): void {

this._appTitleService.setTitleWithTeamId("Clarifications");


//indicate that this Clarifications page is the most recently accessed page
saveCurrentPage(Constants.CLARIFICATIONS_PAGE_KEY);

this.buildForm();
this.loadClars();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { ContestLanguage } from '../models/contest-language';
import { ContestProblem } from '../models/contest-problem';
import { Clarification } from '../models/clarification';
import { DEBUG_MODE } from 'src/constants';

@Injectable({
providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector")
})
export abstract class IContestService {
clarificationsUpdated = new Subject<void>();
contestClock = new Subject<void>();
standingsUpdated = new Subject<void>();
isContestRunning = false;

//give each instance of IContestService a unique ID for debugging purposes
private static nextId: number = 1;
public uniqueId: number;


constructor () {
this.uniqueId = IContestService.nextId++;
if (DEBUG_MODE) {
console.log ("Executing IContestService constructor for unique instance ", this.uniqueId) ;
}
}

abstract getLanguages(): Observable<ContestLanguage[]>;

abstract getProblems(): Observable<ContestProblem[]>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Injectable } from '@angular/core';
import { LoginCredentials } from '../models/login-credentials';
import { Observable, Subject } from 'rxjs';
import { TeamsLoginResponse } from '../models/teams-login-response';
import { Submission } from '../models/submission';
import { Run } from '../models/run';
import { NewClarification } from '../models/new-clarification';
import { DEBUG_MODE } from 'src/constants';

@Injectable({
providedIn: 'root' //forces the service to be a singleton across all app components ('root' == "root injector")
})
export abstract class ITeamsService {
runsUpdated = new Subject<void>();

constructor () {
if (DEBUG_MODE) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of cases where DEBUG_MODE is checked for logging purposes. Could we maybe be create a logging class that when initialized checks the DEBUG_MODE. Having if cases all around makes it harder to read. Logs should be for example used as log.debug("blah blah"). Alternatively we can use winston.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading some forums it is probably a bad idea to use winston on client side.

console.log ("Executing ITeamsService constructor") ;
}
}

abstract login(loginCredentials: LoginCredentials): Observable<TeamsLoginResponse>;

Expand Down
Loading
Loading