parent
a2ca9bb7a4
commit
c5adde5509
@ -0,0 +1,49 @@
|
||||
# Ignore node_modules in the Website folder
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
CTF-Frontend/node_modules
|
||||
CTF-Server/node_modules
|
||||
Website/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
CTF-Frontend/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
#environment files
|
||||
.env
|
||||
|
||||
#Vite
|
||||
|
@ -0,0 +1,17 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
@ -0,0 +1,4 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||
"recommendations": ["angular.ng-template"]
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ng serve",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start",
|
||||
"url": "http://localhost:4200/"
|
||||
},
|
||||
{
|
||||
"name": "ng test",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: test",
|
||||
"url": "http://localhost:9876/debug.html"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
{
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"pattern": "$tsc",
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": "(.*?)"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": "bundle generation complete"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
# CTFFrontend
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.3.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
@ -0,0 +1,104 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"CTF-Frontend": {
|
||||
"projectType": "application",
|
||||
"schematics": {},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/ctf-frontend",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"node_modules/@xterm/xterm/css/xterm.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"options":{
|
||||
"proxyConfig": "./proxy.conf.json"
|
||||
},
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "CTF-Frontend:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "CTF-Frontend:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"node_modules/@xterm/xterm/css/xterm.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "ctf-frontend",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@xterm/addon-attach": "^0.11.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tslib": "^2.3.0",
|
||||
"xterm": "^5.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.3",
|
||||
"@angular/cli": "^19.2.3",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/api": ""
|
||||
},
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"/api/socket.io": {
|
||||
"target": "http://localhost:300",
|
||||
"secure": false,
|
||||
"ws": true,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"/ws": {
|
||||
"target": "ws://localhost:3000",
|
||||
"ws": true,
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,312 @@
|
||||
import { Router } from '@angular/router';
|
||||
import { Contest } from '../models/contest.model';
|
||||
import { Flag } from '../models/flag.model';
|
||||
import { Renderer2, ElementRef } from '@angular/core';
|
||||
import { getEmail, updateContainer, gotoPage } from './Helpers';
|
||||
import { TerminalService } from './terminal.service';
|
||||
import { SocketIOService } from '../notifications/socket-io.service';
|
||||
import { Socket } from 'socket.io-client';
|
||||
|
||||
|
||||
export async function loadPage(renderer: Renderer2, el: ElementRef, ts: any): Promise<void> {
|
||||
const isPractice = window.location.href.includes("Prac");
|
||||
const taskbar: HTMLElement = document.getElementById('Taskbar')!;
|
||||
// basics for use in multiple methods
|
||||
const elements = {
|
||||
flagName: el.nativeElement.querySelector('#Flag_Name') as HTMLElement,
|
||||
desc: el.nativeElement.querySelector('#Desc') as HTMLElement,
|
||||
contName: el.nativeElement.querySelector('#ContHeader') as HTMLElement,
|
||||
contestName: el.nativeElement.querySelector('#ContName') as HTMLElement,
|
||||
contDesc: el.nativeElement.querySelector('#ContDesc') as HTMLElement,
|
||||
hintDesc: el.nativeElement.querySelector('#Hint_Desc') as HTMLElement,
|
||||
HintButts: el.nativeElement.querySelector('#Hint_Butts') as HTMLElement
|
||||
}
|
||||
await LoadContest(el, elements, ts);
|
||||
}
|
||||
|
||||
// get the params to see if admin or not
|
||||
export function getAdmin(): string | null {
|
||||
const url: string = window.location.search;
|
||||
const params = new URLSearchParams(url);
|
||||
return params.get('admin');
|
||||
}
|
||||
|
||||
// load the current active contest
|
||||
async function LoadContest(el: ElementRef, elements: any, ts: any) {
|
||||
const contestID = new URLSearchParams(window.location.search).get('contestID');
|
||||
console.log("Contest ID:", contestID);
|
||||
let contest: Contest | undefined;
|
||||
if(contestID){
|
||||
const data = { contestID: contestID};
|
||||
const res = await fetch('api/contests/getContestByID', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if(res.ok) {
|
||||
contest = await res.json();
|
||||
console.log("contest", contest);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const data = { email: getEmail() };
|
||||
const res = await fetch('api/contests/getActiveContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
contest = await res.json();
|
||||
}
|
||||
}
|
||||
|
||||
const msg = el.nativeElement.querySelector('#DefaultMessage') as HTMLElement;
|
||||
if (!contest) {
|
||||
alert('No Contest is Active');
|
||||
msg.classList.remove('hidden');
|
||||
}
|
||||
else {
|
||||
msg.classList.add('hidden');
|
||||
console.log('Contest:', contest);
|
||||
elements.contName.innerHTML = contest.Name + ' Contest';
|
||||
await LoadElements(contest, el, elements, ts);
|
||||
}
|
||||
}
|
||||
|
||||
// load all the dynamic elements
|
||||
async function LoadElements(contest: Contest, el: ElementRef, elements: any, ts: any) {
|
||||
const data = { contest: contest.ContestID };
|
||||
const res = await fetch('api/flags/getAllFlagsFromContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if(!res.ok){
|
||||
console.error('Error fetching flags for contest');
|
||||
return;
|
||||
}
|
||||
const flagdata: Flag[] = await res.json();
|
||||
console.log("Fetched Flags:", flagdata);
|
||||
const UL = el.nativeElement.querySelector('#FlagList') as HTMLElement;
|
||||
if(UL)
|
||||
UL.innerHTML = '';
|
||||
if(flagdata.length === 0){
|
||||
console.warn("no flags found for contest");
|
||||
return;
|
||||
}
|
||||
if(elements.contName) elements.contName.style.display = 'none';
|
||||
if(elements.contDesc) elements.contDesc.style.display = 'none';
|
||||
|
||||
const tmp = await getActiveFlag(getEmail(), contest.ContestID, el, elements);
|
||||
if(tmp.ActiveFlag === 'ubuntu'){
|
||||
elements.contName.innerHTML = contest.Name;
|
||||
elements.contDesc.innerHTML = contest.Description;
|
||||
elements.contName.style.display = 'block';
|
||||
elements.contDesc.style.display = 'block';
|
||||
}
|
||||
|
||||
const submitFlag = el.nativeElement.querySelector('#Submit_Flag_Stuff') as HTMLElement;
|
||||
const hintStuff = el.nativeElement.querySelector('#Hint_Stuff') as HTMLElement;
|
||||
// add the contest name to the list
|
||||
elements.flagName.textContent = '';
|
||||
elements.desc.textContent = '';
|
||||
if(elements.hintButts) elements.hintButts.innerHTML = '';
|
||||
if(submitFlag) submitFlag.classList.add('hidden');
|
||||
hintStuff.classList.add('hidden');
|
||||
|
||||
flagdata.forEach(flag => {
|
||||
|
||||
console.log(flag);
|
||||
const li = document.createElement('li');
|
||||
const a = document.createElement('a');
|
||||
a.textContent = flag.Name;
|
||||
li.onclick = () => setNewFlag(flag, el, elements, ts);
|
||||
|
||||
// add to list
|
||||
li.appendChild(a);
|
||||
UL.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
// get the active flag of a user
|
||||
async function getActiveFlag(email: string, contest: number, el: ElementRef, elements: any) {
|
||||
const data = { email: email };
|
||||
const res = await fetch('api/flags/getActiveFlag', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) { // get the active flag onto html
|
||||
const ret = await res.json();
|
||||
|
||||
// go through all the flags gotten and make sure it's the right one for this contest
|
||||
for (let i=0; i < ret.length; i++) {
|
||||
if (contest === ret[i].ContestID) setUpFlag(ret[i], el, elements);
|
||||
}
|
||||
}
|
||||
return { ActiveFlag: 'ubuntu' };
|
||||
}
|
||||
let currentFlagID: number | null = null;
|
||||
|
||||
// get the new flag the user clicked on
|
||||
export async function setNewFlag(flag: Flag, el: ElementRef, elements: any, ts: any) {
|
||||
currentFlagID = flag.FlagID;
|
||||
console.log("The new flagID is", currentFlagID);
|
||||
const submitFlagButt = el.nativeElement.querySelector('#Submit_Flag_Stuff') as HTMLElement;
|
||||
const hintstuff = el.nativeElement.querySelector('#Hint_Stuff') as HTMLElement;
|
||||
const resultelement = el.nativeElement.querySelector('#Submission_Result') as HTMLElement;
|
||||
const input = el.nativeElement.querySelector('#Submit_Flag_Stuff input[type="text"]') as HTMLInputElement;
|
||||
|
||||
if(submitFlagButt) {
|
||||
submitFlagButt.classList.remove('hidden');
|
||||
|
||||
// clear the result and the input box
|
||||
if(resultelement){
|
||||
resultelement.textContent = '';
|
||||
resultelement.classList.remove('Incorrect', 'Correct');
|
||||
}
|
||||
|
||||
if(input){
|
||||
input.value = '';
|
||||
input.placeholder = 'NMUCTF${FLAG}';
|
||||
}
|
||||
}
|
||||
if(hintstuff) hintstuff.classList.remove('hidden');
|
||||
|
||||
const email = getEmail();
|
||||
const data = { FlagImage: flag.Image, email: email };
|
||||
const res = await fetch('api/setNewActiveFlag', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) { // get the flag details onto html
|
||||
const ret = await res.json();
|
||||
setUpFlag(flag, el, elements);
|
||||
await updateContainer(email);
|
||||
console.log('Setting new Flag');
|
||||
ts.ClearAndSetTerminal(); // clears the terminal and replaces with the new one for the specific flag
|
||||
}
|
||||
else
|
||||
console.error('Failed to set new flag.');
|
||||
}
|
||||
|
||||
// set the HTML to the correct flag
|
||||
function setUpFlag(flag: Flag, el: ElementRef, elements: any) {
|
||||
|
||||
// set up the basics
|
||||
elements.flagName.innerHTML = flag.Name;
|
||||
elements.desc.innerHTML = flag.Description;
|
||||
if(elements.HintButts) elements.HintButts.innerHTML = '';
|
||||
if(elements.hintDesc) elements.hintDesc.innerHTML = '';
|
||||
|
||||
// set up the hints buttons
|
||||
if (flag.Hint1) {
|
||||
const hint1 = document.createElement('button');
|
||||
hint1.textContent = 'Hint 1';
|
||||
hint1.addEventListener('click', function() {
|
||||
elements.hintDesc.innerHTML = flag.Hint1;
|
||||
});
|
||||
elements.HintButts.appendChild(hint1);
|
||||
}
|
||||
if (flag.Hint2) {
|
||||
const hint2 = document.createElement('button');
|
||||
hint2.textContent = 'Hint 2';
|
||||
hint2.addEventListener('click', function() {
|
||||
elements.hintDesc.innerHTML = flag.Hint2;
|
||||
});
|
||||
elements.HintButts.appendChild(hint2);
|
||||
}
|
||||
if (flag.Hint3) {
|
||||
const hint3 = document.createElement('button');
|
||||
hint3.textContent = 'Hint 3';
|
||||
hint3.addEventListener('click', function() {
|
||||
elements.hintDesc.innerHTML = flag.Hint3;
|
||||
});
|
||||
elements.HintButts.appendChild(hint3);
|
||||
}
|
||||
|
||||
// if theres at least one hint have a placeholder for user
|
||||
if (flag.Hint1 || flag.Hint2 || flag.Hint3) {
|
||||
elements.hintDesc.textContent = 'click to see a Hint';
|
||||
}
|
||||
else {
|
||||
elements.hintDesc.textContent = 'there are no Hints at all good luck :('
|
||||
}
|
||||
}
|
||||
|
||||
/*async function stopContainer(){
|
||||
if(container){
|
||||
await container.kill();
|
||||
await container.remove({force:true});
|
||||
}
|
||||
}*/
|
||||
|
||||
function getCurrentFlagID(){
|
||||
return currentFlagID;
|
||||
}
|
||||
|
||||
export function handleSubmission(el: ElementRef, socket: SocketIOService){
|
||||
const url = window.location.href;
|
||||
if(url.includes("Prac")){
|
||||
checkSubmission(true, el, socket);
|
||||
}
|
||||
else{
|
||||
checkSubmission(false, el, socket);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkSubmission(isPractice: boolean, el: ElementRef, socket: SocketIOService){
|
||||
const submittedFlag = el.nativeElement.querySelector('#Submit_Flag input[type="text"]').value.trim();
|
||||
const email = getEmail();
|
||||
const flagID = getCurrentFlagID();
|
||||
console.log("FLAG ID IS", flagID);
|
||||
const flagName = el.nativeElement.querySelector('#Flag_Name').textContent;
|
||||
const resultelement = el.nativeElement.querySelector('#Submission_Result');
|
||||
if(!submittedFlag){
|
||||
resultelement.textContent = "Please enter a flag.";
|
||||
return;
|
||||
}
|
||||
let res, result;
|
||||
if(!isPractice){
|
||||
res = await fetch('api/checkFlagSubmission', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ email, flagID, submittedFlag })
|
||||
});
|
||||
}
|
||||
else{
|
||||
res = await fetch('api/checkPracSubmission', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ email, flagID, submittedFlag })
|
||||
});
|
||||
}
|
||||
result = await res.json();
|
||||
|
||||
if(result.correct){
|
||||
resultelement.classList.add('Correct');
|
||||
resultelement.classList.remove('Incorrect');
|
||||
resultelement.textContent = 'Correct! :)';
|
||||
|
||||
console.log("SENDING NOTIF", email, flagName);
|
||||
/*socket.emit('submission-notification', {
|
||||
email, flagName, time: new Date().toISOString()
|
||||
});*/
|
||||
}
|
||||
else{
|
||||
resultelement.classList.add('Incorrect');
|
||||
resultelement.classList.remove('Correct');
|
||||
resultelement.textContent = 'Incorrect! :(';
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
/*
|
||||
Jordan Latimer
|
||||
Alex Miller
|
||||
|
||||
Helper functions used by a variety of javascript files
|
||||
*/
|
||||
import { Router } from '@angular/router';
|
||||
import { Contest } from '../models/contest.model';
|
||||
import { Flag } from '../models/flag.model';
|
||||
import { Terminal } from 'xterm';
|
||||
import { AttachAddon } from '@xterm/addon-attach';
|
||||
import { PopulateUserTables } from './User-pfp';
|
||||
let term: Terminal | undefined;
|
||||
// get the email of the client
|
||||
export function getEmail(): string{
|
||||
return sessionStorage.getItem('email') || '';
|
||||
}
|
||||
|
||||
// got to a specific page
|
||||
export function gotoPage(router: Router, page: string): void {
|
||||
router.navigate([page]);
|
||||
}
|
||||
|
||||
export function logOut(router: Router){
|
||||
router.navigate(['']);
|
||||
}
|
||||
|
||||
// popup window for a specific page with the email
|
||||
export function Popup(site: string): void {
|
||||
|
||||
const width = 500;
|
||||
const height = 500;
|
||||
|
||||
// get the right placement and size for any screen
|
||||
const x = screenX + (window.screen.width / 2) - (width / 2);
|
||||
const y = screenY + (window.screen.height / 2) - (height / 2);
|
||||
// add the features and the email inside the URL and open the window
|
||||
const features = `width=${width},height=${height},left=${x},top=${y}`;
|
||||
window.open(site,'popup',features);
|
||||
}
|
||||
|
||||
// get all the table contents and add them in the respective table
|
||||
export async function PopulateTable(page: number, InsertIntoTable: (data: Contest[]) => void): Promise<void> {
|
||||
const data = { email: getEmail() };
|
||||
const res = await fetch('api/contests/getContests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
const list = await res.json();
|
||||
if (page === 0) InsertIntoTable(list);
|
||||
else PopulateUserTables(list);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getContests(): Promise<Contest[]> {
|
||||
const data = { email: getEmail() };
|
||||
const res = await fetch('api/contests/getContests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
const list: Contest[] = await res.json();
|
||||
return list;
|
||||
}
|
||||
else {
|
||||
console.error('Failed to fetch contests');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const flagItems = document.querySelectorAll('#Flag_Sidebar li');
|
||||
flagItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
flagItems.forEach(f => f.classList.remove('selected-flag'));
|
||||
item.classList.add('selected-flag');
|
||||
})
|
||||
});
|
||||
|
||||
export async function InsertIntoTable(data: Contest[], router: Router): Promise<void> {
|
||||
// Past Contest Table
|
||||
const PCT = document.querySelector('#Past_Contests tbody') as HTMLTableSectionElement | null;
|
||||
// Active Contest Table
|
||||
const ACT = document.querySelector('#Current_Contest tbody') as HTMLTableSectionElement | null;
|
||||
if(!PCT || !ACT)
|
||||
return;
|
||||
PCT.innerHTML = '';
|
||||
ACT.innerHTML = '';
|
||||
|
||||
// list of flags
|
||||
const res2 = await fetch('api/flags/getAllFlags');
|
||||
const flaglist: Flag[] = await res2.json();
|
||||
|
||||
const email = getEmail();
|
||||
const res3 = await fetch('api/contests/getActiveContest', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({email})
|
||||
})
|
||||
let activeContest = await res3.json().catch(() => null);
|
||||
if(activeContest && activeContest.error === 'NO CONTEST FOUND') activeContest = null;
|
||||
data.forEach((contest: Contest) => {
|
||||
const row = document.createElement('tr');
|
||||
const name = document.createElement('td');
|
||||
const flags = document.createElement('td');
|
||||
const leader = document.createElement('td');
|
||||
|
||||
name.textContent = contest.Name;
|
||||
|
||||
// get the amount of flags in the table
|
||||
let i = 0;
|
||||
flaglist.forEach(flag => {
|
||||
if (flag.ContestID === contest.ContestID) i++;
|
||||
});
|
||||
flags.textContent = i.toString();
|
||||
|
||||
// get the button for the leaderboard
|
||||
const leaderbutton = document.createElement('button');
|
||||
leaderbutton.textContent = 'view';
|
||||
leaderbutton.addEventListener('click', function() {
|
||||
// Popup('leaderboard.html' ,getEmail(), 'ContestName=' + contest.Name); Uncomment when we finish leaderboard implementation
|
||||
});
|
||||
leader.appendChild(leaderbutton);
|
||||
|
||||
row.appendChild(name);
|
||||
row.appendChild(flags);
|
||||
|
||||
if (contest.IsActive === 1) {
|
||||
const button = document.createElement('td');
|
||||
const JoinButton = document.createElement('button');
|
||||
JoinButton.textContent = 'Join';
|
||||
JoinButton.addEventListener('click', function() {
|
||||
router.navigate(['/contest-page']);
|
||||
});
|
||||
|
||||
button.appendChild(JoinButton);
|
||||
row.appendChild(button);
|
||||
ACT.appendChild(row);
|
||||
}
|
||||
else {
|
||||
row.appendChild(leader);
|
||||
if(!activeContest){
|
||||
const pracButtCell = document.createElement('td');
|
||||
const pracButt = document.createElement('button');
|
||||
pracButt.textContent = 'Practice';
|
||||
pracButt.addEventListener('click', function() {
|
||||
console.log("contestID", contest.ContestID);
|
||||
router.navigate(['/contest-page'], { queryParams: { email: email, contestID: contest.ContestID, Prac: true } });
|
||||
});
|
||||
pracButtCell.appendChild(pracButt);
|
||||
row.appendChild(pracButtCell);
|
||||
}
|
||||
PCT.appendChild(row);
|
||||
}
|
||||
});
|
||||
if(!activeContest){
|
||||
const pracRow = document.querySelector('#Past_Contests thead tr');
|
||||
if(pracRow){
|
||||
const pracRowCell = document.createElement('th');
|
||||
pracRowCell.textContent = 'Practice';
|
||||
pracRow.appendChild(pracRowCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update the container
|
||||
export async function updateContainer(email: string){
|
||||
const res = await fetch(`api/updateContainer/${email}`, { method: 'POST'});
|
||||
|
||||
if(res.ok){
|
||||
console.log('Container updated successfully');
|
||||
}
|
||||
|
||||
else
|
||||
console.log('Failed to update container');
|
||||
}
|
||||
|
||||
// reattach the terminal web socket
|
||||
export async function reattachTerminal(term: Terminal){
|
||||
console.log('reattaching termnial');
|
||||
|
||||
if(term) {
|
||||
term.dispose();
|
||||
}
|
||||
term = new Terminal();
|
||||
const container = document.getElementById('Linux_Shell');
|
||||
if(container && term) {
|
||||
term.open(container);
|
||||
}
|
||||
const socket = new WebSocket('/ws');
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify(getEmail()));
|
||||
console.log('Websocket opened');
|
||||
}
|
||||
const attachAddon = new AttachAddon(socket);
|
||||
term.loadAddon(attachAddon);
|
||||
term.focus();
|
||||
socket.onclose = () => {
|
||||
console.log("socket closed");
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Client code for Profile Page for Users
|
||||
*/
|
||||
import { Contest } from '../models/contest.model';
|
||||
import { Flag } from '../models/flag.model';
|
||||
import { User } from '../models/user.model';
|
||||
import { getEmail, Popup, InsertIntoTable, getContests } from './Helpers';
|
||||
|
||||
// sets of names to pull randomly from for random names
|
||||
let Adjectives = [
|
||||
'Aspiring', 'Strict','Electronic','Talented','Gifted','Wonderful',
|
||||
'Amazing','Inspiring','First','Second','Third','Cowardly','True',
|
||||
'Coordinated','Sneaky','Concerned','Courageous','Nice','Mean','Sharp',
|
||||
'Mysterious','Magical','Tame','Well-off','Overconfident','Cautious',
|
||||
'Wandering','Wrathful','Important','Jolly','Mistaken','Admirable'
|
||||
]
|
||||
let Animals = [
|
||||
'Fox','Turtle','Penguin','Robin','Shark','Giraffe','Kangaroo','Cat','Dog',
|
||||
'Fish','Bear','Lion','Hippo','Duck','Octopus','Elephant','Bunny','Rabbit',
|
||||
'Mouse','Pig','Cow','Chicken','Rooster','Ladybug','Firefly','Mosquito','Snail',
|
||||
'Koala','Platypus','Woodchuck','Polarbear','Rat','Bull','Newt','Deer','Fawn',
|
||||
'Cheetah','Leopard','Beaver','Llama','Guineapig','Squirrel','Eagle','Moose'
|
||||
]
|
||||
let Colors = [
|
||||
'Green', 'Blue','Aqua','Red','Purple','Pink','Gray','Black','Brown','Teal',
|
||||
'Limegreen','White','Yellow','Maroon','Violet', 'Coral','Emerald','Orange',
|
||||
'Darkgreen','Navyblue','Cyan','Crimson','Bronze','Gold','Silver','Amber',
|
||||
'Turquoise','Indigo','Fuchsia','Magenta','Platinum','Tan','Jade','Ruby'
|
||||
]
|
||||
let Things = [
|
||||
'Chair','Desk','Lamp','Keys','Bottle','Wall','Window','Barn','House','Door',
|
||||
'Outlet','TV','Screen','Road','Lightbulb','Sidewalk','Keyboard','Paper','Plastic',
|
||||
'Remote','Phone','Computer','Can','Table','Backpack','Fan','Carpet','Pen','Pencil',
|
||||
'Notebook','Clock','Tree','Pants','Shorts','Flannel','Jacket','Apple','Banana','Watch',
|
||||
'Apricot','Socks','Pizza','Pasty','Car','Airplane','Bus','Scissors','Rock','Water'
|
||||
]
|
||||
let Name = '';
|
||||
|
||||
// on load, populate tables and set the users name inside the input field
|
||||
export async function loadTable(): Promise<string> {
|
||||
let contests = await getContests();
|
||||
PopulateUserTables(contests);
|
||||
const data = { email: getEmail() };
|
||||
const res = await fetch('api/users/getUsername', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
if (res.ok) {
|
||||
const ret = await res.json();
|
||||
return ret;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// populate both user tables
|
||||
export async function PopulateUserTables(PCData: Contest[]): Promise<void> {
|
||||
const TB = document.querySelector('#Past_Contests tbody') as HTMLElement;
|
||||
TB.innerHTML = "";
|
||||
|
||||
// get all of the flags
|
||||
const res = await fetch('api/flags/getAllFlags');
|
||||
const PCData2: Flag[] = await res.json();
|
||||
let totalflagamount = 0;
|
||||
|
||||
// insert the amount of flags per contests
|
||||
PCData.forEach(contest => {
|
||||
let flagamount = 0;
|
||||
const row = document.createElement('tr');
|
||||
const name = document.createElement('td');
|
||||
name.textContent = contest.Name;
|
||||
const flags = document.createElement('td');
|
||||
PCData2.forEach(flag => {
|
||||
if (flag.ContestID === contest.ContestID) flagamount++;
|
||||
});
|
||||
flags.textContent = flagamount.toString();
|
||||
totalflagamount += flagamount;
|
||||
|
||||
const leader = document.createElement('td');
|
||||
|
||||
// get the button for the leaderboard
|
||||
const leaderbutton = document.createElement('button');
|
||||
leaderbutton.textContent = 'view';
|
||||
leaderbutton.setAttribute('id','ViewLeaderboard');
|
||||
leaderbutton.addEventListener('click', function() {
|
||||
//Popup('leaderboard.html' ,getEmail(), 'ContestName=' + contest.Name); Uncomment when leaderboard is implemented
|
||||
});
|
||||
leader.appendChild(leaderbutton);
|
||||
|
||||
|
||||
// add to the table
|
||||
row.appendChild(name);
|
||||
row.appendChild(flags);
|
||||
row.appendChild(leader);
|
||||
|
||||
TB.appendChild(row);
|
||||
});
|
||||
|
||||
// get the user information
|
||||
const data = { email: getEmail() };
|
||||
const res2 = await fetch('api/users/getUser', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res2.ok) {
|
||||
const Rdata = await res2.json();
|
||||
|
||||
const TB2 = document.querySelector('#Record tbody') as HTMLTableSectionElement;
|
||||
if(TB2)
|
||||
TB2.innerHTML = '';
|
||||
|
||||
// create the table for the users record, flags completed, and total flags
|
||||
const row = document.createElement('tr');
|
||||
const Total = document.createElement('td');
|
||||
const Completed = document.createElement('td');
|
||||
const record = document.createElement('td');
|
||||
Total.textContent = totalflagamount.toString();
|
||||
Completed.textContent = Rdata.Flags;
|
||||
record.textContent = Completed.innerHTML + ' / ' + Total.innerHTML;
|
||||
|
||||
row.appendChild(Completed);
|
||||
row.appendChild(Total);
|
||||
row.appendChild(record);
|
||||
|
||||
console.log('TB2:', TB2);
|
||||
console.log('row:', row);
|
||||
TB2.appendChild(row);
|
||||
}
|
||||
else {
|
||||
console.error('Error getting data for Flag Users');
|
||||
}
|
||||
}
|
||||
|
||||
// get a new name from name array
|
||||
export function getNewName(): string {
|
||||
|
||||
let first = '';
|
||||
let second = '';
|
||||
|
||||
// get what arrays to be used
|
||||
let firstarr = Math.floor(Math.random() * 2);
|
||||
let secondarr = Math.floor(Math.random() * 2);
|
||||
|
||||
// get the random array items based on the arrays used
|
||||
if (firstarr === 0) first = Adjectives[Math.floor(Math.random() * Adjectives.length)];
|
||||
else first = Colors[Math.floor(Math.random() * Colors.length)];
|
||||
|
||||
if (secondarr === 0) second = Animals[Math.floor(Math.random() * Animals.length)];
|
||||
else second = Things[Math.floor(Math.random() * Things.length)];
|
||||
|
||||
// set up the name
|
||||
Name = '';
|
||||
Name += first + second;
|
||||
|
||||
// determine if there will be a random number after and add it
|
||||
let num = Math.floor(Math.random() * 2);
|
||||
if (num === 0) Name += Math.floor(Math.random() * 1001);
|
||||
|
||||
return Name;
|
||||
}
|
||||
|
||||
// set the random name to the name of the user
|
||||
export async function setNewName(NameInput: string): Promise<void> {
|
||||
const name = NameInput;
|
||||
const data = { name: name, email: getEmail() };
|
||||
const res = await fetch('api/users/setUserName', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TerminalService } from './terminal.service';
|
||||
|
||||
describe('TerminalService', () => {
|
||||
let service: TerminalService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(TerminalService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Service file for Terminals
|
||||
*/
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { AttachAddon } from '@xterm/addon-attach';
|
||||
import { getEmail } from './Helpers';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TerminalService {
|
||||
|
||||
private term: Terminal; // terminal for container
|
||||
private socket: WebSocket | null = null; // websocket attached to container
|
||||
|
||||
constructor() {
|
||||
this.term = new Terminal();
|
||||
}
|
||||
|
||||
// get the terminal
|
||||
getTermainal() : Terminal {
|
||||
return this.term;
|
||||
}
|
||||
|
||||
// set the terminal
|
||||
setTerminal(newTerm: Terminal) : void {
|
||||
this.term = newTerm;
|
||||
}
|
||||
|
||||
ClearTerminal() : void {
|
||||
if(this.term){
|
||||
this.term.write("\x1b[2J");
|
||||
this.term.write("\x1b[H");
|
||||
this.term.write('Loading Environment...');
|
||||
}
|
||||
}
|
||||
|
||||
// create terminal
|
||||
CreateTerminal(): void {
|
||||
const container = document.getElementById('Linux_Shell');
|
||||
if (container && this.term) {
|
||||
this.term.open(container);
|
||||
this.CreateWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
// create the web socket connection
|
||||
CreateWebSocket() {
|
||||
this.socket = new WebSocket('/ws');
|
||||
this.socket.onopen = () => {
|
||||
if (this.socket) {
|
||||
this.socket.send(JSON.stringify(getEmail()));
|
||||
console.log('WebSocket opened');
|
||||
|
||||
setTimeout(() => {
|
||||
const attachAddon = new AttachAddon(this.socket!);
|
||||
this.term.loadAddon(attachAddon);
|
||||
this.term.focus();
|
||||
console.log('reattached attachAddon');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// reattach the terminal for a new flag
|
||||
reattachTerminal() {
|
||||
if (this.term) {
|
||||
this.term.dispose();
|
||||
this.term = new Terminal();
|
||||
}
|
||||
|
||||
this.CreateTerminal(); // create the new terminal
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { TreeService } from './tree.service';
|
||||
|
||||
describe('TreeService', () => {
|
||||
let service: TreeService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(TreeService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,117 @@
|
||||
import { Injectable, InputSignal, Inject } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class TreeService {
|
||||
|
||||
private name: string;
|
||||
private contents: string;
|
||||
private children: TreeService[];
|
||||
private parentnode: TreeService | null;
|
||||
private directory: boolean;
|
||||
private nodenum: number;
|
||||
|
||||
constructor(@Inject(String) name: string, @Inject(String) contents: string, @Inject(Boolean) directory: boolean, @Inject(Number) nodenum: number, @Inject(TreeService) parentnode: TreeService | null) {
|
||||
this.name = name;
|
||||
this.contents = contents;
|
||||
this.children = [];
|
||||
this.parentnode = parentnode;
|
||||
this.directory = directory;
|
||||
this.nodenum = nodenum;
|
||||
}
|
||||
|
||||
// adding a child node to tree
|
||||
AddChild(node: TreeService) {
|
||||
this.children.push(node);
|
||||
}
|
||||
|
||||
// chekc if a child already has the same name
|
||||
IsNameTaken(filename: string): boolean {
|
||||
// check children
|
||||
if (this.children.length > 0) {
|
||||
for (var i=0; i < this.children.length; i++) {
|
||||
let child = this.children[i];
|
||||
|
||||
// check the name
|
||||
if (child.name === filename) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
// get the node with the correct name and nodenum
|
||||
getNodeByName(name: string, num: number): TreeService | null{
|
||||
|
||||
// base case
|
||||
if (this.name === name && this.nodenum == num) return this;
|
||||
|
||||
// Traverse tree structure
|
||||
if (this.children.length > 0) {
|
||||
for (var i=0; i < this.children.length; i++) {
|
||||
const childResult = this.children[i].getNodeByName(name,num);
|
||||
if (childResult !== null && childResult.name === name && childResult.nodenum == num) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// get all the nodes in the tree with the same name
|
||||
getNodesSameName(name: string, nodes: TreeService[]): TreeService[] {
|
||||
|
||||
// base case
|
||||
if (this.name === name) nodes.push(this);
|
||||
|
||||
// check children
|
||||
if (this.children.length > 0) {
|
||||
for (var i=0; i < this.children.length; i++) {
|
||||
this.children[i].getNodesSameName(name,nodes);
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
// delete the parent references to send tree to server
|
||||
DeleteParentRef() {
|
||||
|
||||
this.parentnode = null;
|
||||
|
||||
if (this.children.length > 0) {
|
||||
for (var i=0; i < this.children.length; i++) {
|
||||
this.children[i].DeleteParentRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete all children of this node
|
||||
DeleteTheChildren() {
|
||||
this.children = [];
|
||||
}
|
||||
|
||||
// gets
|
||||
|
||||
getDir() : boolean {
|
||||
return this.directory;
|
||||
}
|
||||
getName() : string{
|
||||
return this.name;
|
||||
}
|
||||
getNodeNum() : number {
|
||||
return this.nodenum;
|
||||
}
|
||||
getChildren() : TreeService[] {
|
||||
return this.children;
|
||||
}
|
||||
getParent() : TreeService | null {
|
||||
return this.parentnode;
|
||||
}
|
||||
|
||||
// sets
|
||||
|
||||
setChildren(newchildren: TreeService[]) {
|
||||
this.children = newchildren;
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
html {
|
||||
background-color: green;
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
form {
|
||||
text-align: center;
|
||||
}
|
||||
h1, h3 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
#Inputs {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
grid-gap: 5px;
|
||||
margin-left: 25%;
|
||||
margin-top: 25%;
|
||||
}
|
||||
label {
|
||||
text-align: right;
|
||||
}
|
||||
#Submit {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
#submit * {
|
||||
width: 15%;
|
||||
height: 5%;
|
||||
}
|
||||
#Submit button {
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#hint1Text, #hint2Text, #hint3Text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the respective hint fields based on the selected radio button */
|
||||
#hint1:checked ~ #hint1Text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#hint2:checked ~ #hint1Text,
|
||||
#hint2:checked ~ #hint2Text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#hint3:checked ~ #hint1Text,
|
||||
#hint3:checked ~ #hint2Text,
|
||||
#hint3:checked ~ #hint3Text{
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* for Create Image */
|
||||
#body {
|
||||
display: grid;
|
||||
grid-template-rows: 25% auto auto auto auto auto 25%;
|
||||
grid-template-columns: 20% 20% 20% 20% 20%;
|
||||
justify-items: center;
|
||||
}
|
||||
#labels {
|
||||
grid-row: 1/2;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
#Butts {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
align-self: flex-end;
|
||||
}
|
||||
#TextBox {
|
||||
grid-row: 3/6;
|
||||
grid-column: 1/3;
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
#DropBox {
|
||||
grid-row: 3/6;
|
||||
grid-column: 3/4;
|
||||
border: 3px black dashed;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
#SendFile {
|
||||
grid-row: 6/7;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
#FileTree {
|
||||
grid-row: 3/6;
|
||||
grid-column: 4/6;
|
||||
}
|
||||
#Submition {
|
||||
grid-row: 7/8;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
.selected-node {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
<!--
|
||||
Jordan Latimer
|
||||
Alex Miller
|
||||
|
||||
Adding a contest screen for Admin
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title> Add Contest</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Add Contest </h1>
|
||||
|
||||
<!-- form for inserting the contest -->
|
||||
<form id="Inputs">
|
||||
<label> Name: </label>
|
||||
<input id="Name" type="text" align="middle" required>
|
||||
<label> Description: </label>
|
||||
<input id="Desc" type="text" align="middle" required>
|
||||
</form>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- submitting and closing -->
|
||||
<div id="Submit">
|
||||
<input id="AddContest" type="button" value="Add Contest" (click)="AddContest();">
|
||||
<button onclick="self.close();"> Cancel </button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddContestComponent } from './add-contest.component';
|
||||
|
||||
describe('AddContestComponent', () => {
|
||||
let component: AddContestComponent;
|
||||
let fixture: ComponentFixture<AddContestComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AddContestComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddContestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { getEmail } from '../Helper/Helpers';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-contest',
|
||||
imports: [],
|
||||
templateUrl: './add-contest.component.html',
|
||||
styleUrl: './add-contest.component.css'
|
||||
})
|
||||
export class AddContestComponent {
|
||||
|
||||
// add a contest to the database
|
||||
async AddContest() {
|
||||
const nameInput = document.getElementById("Name") as HTMLInputElement;
|
||||
const descInput = document.getElementById("Desc") as HTMLTextAreaElement;
|
||||
const name = nameInput.value;
|
||||
const desc = descInput.value;
|
||||
console.log("Name entered: ", name);
|
||||
console.log("Description entered: ", desc);
|
||||
const isActive = 0;
|
||||
console.log("EMAIL IS:", this.getEmail())
|
||||
const data = {Name: name, IsActive: isActive, email: this.getEmail(), Desc: desc};
|
||||
const res = await fetch('api/AddContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
console.log("Success: adding contest");
|
||||
window.opener.location.reload();
|
||||
window.close();
|
||||
}
|
||||
else {
|
||||
console.log("ERROR: adding contest");
|
||||
}
|
||||
}
|
||||
|
||||
getEmail(): string{
|
||||
return getEmail();
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Add Flag popup window
|
||||
*/
|
||||
|
||||
#FlagInputs {
|
||||
text-align: center;
|
||||
background-color: lightgreen;
|
||||
padding: 10px;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
#Submit {
|
||||
text-align: center;
|
||||
justify-items: center;
|
||||
}
|
||||
button {
|
||||
font-weight: bold;
|
||||
height: 15%;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#flagName {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
select {
|
||||
display: inline-block;
|
||||
width: 170px;
|
||||
margin: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
option {
|
||||
text-align: center;
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
<!--
|
||||
Jordan Latimer
|
||||
Alex Miller
|
||||
|
||||
Adding a Flag for Admin
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title> Add Flag</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Add Flag </h1>
|
||||
|
||||
<!-- form for adding the flag -->
|
||||
<form id="FlagInputs">
|
||||
<label> Name: </label>
|
||||
<input id="Name" type="text" align="middle" required>
|
||||
<br><br>
|
||||
<label> Description: </label>
|
||||
<input id="Description" required>
|
||||
<br><br>
|
||||
|
||||
<input type="radio" id="hint1" name="hintCount" value="1">
|
||||
<label for="hint1">1 Hint</label>
|
||||
<br>
|
||||
|
||||
<input type="radio" id="hint2" name="hintCount" value="2">
|
||||
<label for="hint2">2 Hints</label>
|
||||
<br>
|
||||
|
||||
<input type="radio" id="hint3" name="hintCount" value="3">
|
||||
<label for="hint3">3 Hints</label>
|
||||
<br><br>
|
||||
|
||||
<div id = "hint1Text">
|
||||
<label for="Hint1">Hint 1: </label>
|
||||
<input id="Hint1" type="text"><br><br>
|
||||
</div>
|
||||
|
||||
<div id = "hint2Text">
|
||||
<label for="Hint2">Hint 2: </label>
|
||||
<input id="Hint2" type="text"><br><br>
|
||||
</div>
|
||||
|
||||
<div id = "hint3Text">
|
||||
<label for="Hint3">Hint 3: </label>
|
||||
<input id="Hint3" type="text"><br><br>
|
||||
</div>
|
||||
|
||||
<!-- image of the flag to create container -->
|
||||
<label> Image: </label>
|
||||
<select id="Images">
|
||||
<option> ubuntu </option>
|
||||
</select>
|
||||
<br><br>
|
||||
|
||||
<!-- path to place the flag into -->
|
||||
<label> Path To File </label>
|
||||
<input id="Path" type="text">
|
||||
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- submit button just closes the window for now -->
|
||||
<div id="Submit">
|
||||
<button id="flagName" type="submit" (click)="AddFlag()">Add Flag</button>
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddFlagComponent } from './add-flag.component';
|
||||
|
||||
describe('AddFlagComponent', () => {
|
||||
let component: AddFlagComponent;
|
||||
let fixture: ComponentFixture<AddFlagComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AddFlagComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddFlagComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { gotoPage } from '../Helper/Helpers';
|
||||
import { Router } from '@angular/router';
|
||||
@Component({
|
||||
selector: 'app-add-flag',
|
||||
imports: [],
|
||||
templateUrl: './add-flag.component.html',
|
||||
styleUrl: './add-flag.component.css'
|
||||
})
|
||||
export class AddFlagComponent {
|
||||
|
||||
constructor(private router: Router){}
|
||||
|
||||
// add a flag to a contest
|
||||
async AddFlag(){
|
||||
// get contestId from URL
|
||||
const contestId = parseInt(sessionStorage.getItem('selectedContestID') || '', 10);
|
||||
|
||||
// get all the values in the form
|
||||
const flagName = document.getElementById("Name") as HTMLInputElement;
|
||||
const description = document.getElementById("Description") as HTMLInputElement;
|
||||
let Hint1 = document.getElementById("Hint1") as HTMLInputElement;
|
||||
let Hint2 = document.getElementById("Hint2") as HTMLInputElement;
|
||||
let Hint3 = document.getElementById("Hint3") as HTMLInputElement;
|
||||
const image = document.getElementById('Images') as HTMLInputElement;
|
||||
const path = document.getElementById('Path') as HTMLInputElement;
|
||||
|
||||
// required fields
|
||||
if(!flagName.value){
|
||||
alert("Enter a flag name.");
|
||||
return;
|
||||
}
|
||||
if(contestId === null || contestId === undefined){
|
||||
alert("Please select a contest first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// add the flag
|
||||
const data = {name: flagName.value, desc: description.value, contest: contestId, image: image.value, path: path.value, hint1: Hint1.value || '', hint2: Hint2.value || '', hint3: Hint3.value || ''};
|
||||
try{
|
||||
const response = await fetch('api/AddFlag', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json',},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if(response.ok){
|
||||
console.log("Flag added");
|
||||
// reload the parent window when this window closes
|
||||
window.opener.postMessage({ type: 'FLAG_ADDED', contestId: contestId }, window.origin);
|
||||
window.close();
|
||||
}
|
||||
else{
|
||||
alert("Failed to add flag.");
|
||||
}
|
||||
} catch(error){
|
||||
console.error("Error adding flag:", error);
|
||||
alert("An error occurred while adding flag.");
|
||||
}
|
||||
}
|
||||
|
||||
navtoPageCI(){
|
||||
gotoPage(this.router, '/create-image');
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
html {
|
||||
background-color: green;
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
form {
|
||||
text-align: center;
|
||||
}
|
||||
h1, h3 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
#Inputs {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
grid-gap: 5px;
|
||||
margin-left: 25%;
|
||||
margin-top: 25%;
|
||||
}
|
||||
label {
|
||||
text-align: right;
|
||||
}
|
||||
#Submit {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
#submit * {
|
||||
width: 15%;
|
||||
height: 5%;
|
||||
}
|
||||
#Submit button {
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#hint1Text, #hint2Text, #hint3Text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the respective hint fields based on the selected radio button */
|
||||
#hint1:checked ~ #hint1Text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#hint2:checked ~ #hint1Text,
|
||||
#hint2:checked ~ #hint2Text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#hint3:checked ~ #hint1Text,
|
||||
#hint3:checked ~ #hint2Text,
|
||||
#hint3:checked ~ #hint3Text{
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* for Create Image */
|
||||
#body {
|
||||
display: grid;
|
||||
grid-template-rows: 25% auto auto auto auto auto 25%;
|
||||
grid-template-columns: 20% 20% 20% 20% 20%;
|
||||
justify-items: center;
|
||||
}
|
||||
#labels {
|
||||
grid-row: 1/2;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
#Butts {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
align-self: flex-end;
|
||||
}
|
||||
#TextBox {
|
||||
grid-row: 3/6;
|
||||
grid-column: 1/3;
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
#DropBox {
|
||||
grid-row: 3/6;
|
||||
grid-column: 3/4;
|
||||
border: 3px black dashed;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
#SendFile {
|
||||
grid-row: 6/7;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
#FileTree {
|
||||
grid-row: 3/6;
|
||||
grid-column: 4/6;
|
||||
}
|
||||
#Submition {
|
||||
grid-row: 7/8;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
.selected-node {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Adding a student page for Admin
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title> Add Student </title>
|
||||
</head>
|
||||
<body>
|
||||
<h1> Add Student </h1>
|
||||
|
||||
<!-- form for adding a student -->
|
||||
<form id="Inputs">
|
||||
<label> Name: </label>
|
||||
<input id="Name" type="text" [(ngModel)]="Uname" name="Uname" align="middle">
|
||||
<label> Email: </label>
|
||||
<input id="EM" type="text" [(ngModel)]="email" name="email" align="middle">
|
||||
<label> Password: </label>
|
||||
<input *ngIf="!showPass" id="PS1" type="password" [(ngModel)]="password" name="password" align="middle">
|
||||
<input *ngIf="showPass" id="PS1" type="text" [(ngModel)]="password" name="password" align="middle">
|
||||
<label> Confirm Password: </label>
|
||||
<input *ngIf="!showPass" id="PS2" type="password" [(ngModel)]="confirmPassword" name="confirmPassword" align="middle">
|
||||
<input *ngIf="showPass" id="PS2" type="text" [(ngModel)]="confirmPassword" name="confirmPassword" align="middle">
|
||||
<label> Show Password </label>
|
||||
<input type="checkbox" (click)="ShowPassword()" id="ShowPass">
|
||||
</form>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- submitting or closing-->
|
||||
<div id="Submit">
|
||||
<input id="AddStudent" type="submit" (click)="AddStudent()">
|
||||
<button onclick="self.close();"> Cancel </button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddStudentComponent } from './add-student.component';
|
||||
|
||||
describe('AddStudentComponent', () => {
|
||||
let component: AddStudentComponent;
|
||||
let fixture: ComponentFixture<AddStudentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AddStudentComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddStudentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { getEmail } from '../Helper/Helpers';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-student',
|
||||
imports: [NgIf, FormsModule],
|
||||
templateUrl: './add-student.component.html',
|
||||
styleUrl: './add-student.component.css'
|
||||
})
|
||||
export class AddStudentComponent {
|
||||
Uname: string = '';
|
||||
email: string = '';
|
||||
password: string = '';
|
||||
confirmPassword: string = '';
|
||||
showPass: boolean = false;
|
||||
|
||||
ShowPassword() {
|
||||
this.showPass = !this.showPass
|
||||
}
|
||||
|
||||
// add a student to the database
|
||||
async AddStudent() {
|
||||
const Aemail = getEmail();
|
||||
|
||||
// password protection
|
||||
if (this.password.length < 10 || this.password == "") {
|
||||
alert("password must be at least 10 characters");
|
||||
return;
|
||||
}
|
||||
else if(this.email == Aemail){
|
||||
alert("Funny guy, eh?");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.password == this.confirmPassword) { // passwords match
|
||||
const data = {name: this.Uname, email: this.email, Aemail: Aemail, password: this.password};
|
||||
const res = await fetch('api/AddStudent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
console.log("Success: adding student");
|
||||
window.opener.location.reload();
|
||||
window.close();
|
||||
}
|
||||
else {
|
||||
console.log("ERROR: adding student");
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert("Passowrds do not match");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
/*
|
||||
Jordan Latimer
|
||||
Alex Miller
|
||||
|
||||
CSS for both Contest Pages
|
||||
*/
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
background-color: green;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: 15% 10% auto 20%;
|
||||
grid-template-rows: 10% 40% 10% auto;
|
||||
}
|
||||
|
||||
/* Name of the contest at the top left */
|
||||
#Contest_Name {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/3;
|
||||
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 175%;
|
||||
}
|
||||
#Contest_Name * {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* sidebar thats scrollable for different flags */
|
||||
#Flag_Sidebar {
|
||||
background-color: white;
|
||||
grid-row: 2/5;
|
||||
grid-column: 1/2;
|
||||
height: 100%;
|
||||
}
|
||||
#Flag_Sidebar ul {
|
||||
list-style-type: none;
|
||||
height: 80%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
background-color: white;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* details for each flag (name, description, etc) */
|
||||
#Flag_Deats {
|
||||
background-color: white;
|
||||
grid-row: 2/3;
|
||||
grid-column: 2/4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#Notifications {
|
||||
background-color: rgba(65, 145, 55, 0.511);
|
||||
margin-left: 50%;
|
||||
grid-row: 3/4;
|
||||
grid-column: 3/5;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#Notifications.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
/* Submitting the flag */
|
||||
#Submit_Flag {
|
||||
background-color: white;
|
||||
grid-row: 3/4;
|
||||
grid-column: 2/4;
|
||||
|
||||
}
|
||||
#Submit_Flag_Stuff * {
|
||||
height: 40%;
|
||||
width: 15%;
|
||||
}
|
||||
#Submit_Butt {
|
||||
background-color: lawngreen;
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
}
|
||||
#Submit_Flag h3 {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
display: inline;
|
||||
}
|
||||
.Incorrect, .Correct {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 125%;
|
||||
}
|
||||
.Incorrect {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
.Correct {
|
||||
background-color: lawngreen;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* hints for each flag */
|
||||
#Hints {
|
||||
background-color: white;
|
||||
grid-row: 2/4;
|
||||
grid-column: 4/5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: white;
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Linux Shell for commands */
|
||||
#Linux_Shell {
|
||||
grid-row: 4/5;
|
||||
grid-column: 2/5;
|
||||
color: lawngreen;
|
||||
}
|
||||
|
||||
/* when a flag is selected */
|
||||
.selected-flag {
|
||||
background-color: lightgray;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Flags and Hints Labels */
|
||||
.FlagHint {
|
||||
background-color: black;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
font-size: 200%;
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Contest Page for Admins
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title> NMU CTF Contest Page </title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Bar at top for moving tabs -->
|
||||
<div id="Taskbar">
|
||||
<a (click)="confirmSelection()"> Profile </a>
|
||||
</div>
|
||||
|
||||
<!-- Contest name -->
|
||||
<div id="Contest_Name">
|
||||
<h1 id="ContHeader"></h1>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar thats scrollable for different flags -->
|
||||
<div id="Flag_Sidebar">
|
||||
<h2 class="FlagHint">Flags</h2>
|
||||
<ul id="FlagList">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- details for each flag (name, description etc) -->
|
||||
<div id="Flag_Deats">
|
||||
<div id ="DefaultMessage">
|
||||
<h2>Welcome to NMU CTF</h2>
|
||||
<p>There are currently no contests active at this time</p>
|
||||
</div>
|
||||
<h2 id="ContName"></h2>
|
||||
<p id="ContDesc"></p>
|
||||
<h2 id="Flag_Name"></h2>
|
||||
<p id="Desc" ></p>
|
||||
</div>
|
||||
|
||||
<div id="Submit_Flag">
|
||||
|
||||
</div>
|
||||
<!-- Hints for each flag -->
|
||||
<div id="Hints">
|
||||
<div id="Hint_Stuff">
|
||||
<h2 class="FlagHint"> Hints </h2>
|
||||
<div id="Hint_Butts">
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<textarea readonly rows="15" cols="30" id="Hint_Desc"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Linux Shell for commands -->
|
||||
<div id="Linux_Shell"></div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminContestComponent } from './admin-contest.component';
|
||||
|
||||
describe('AdminContestComponent', () => {
|
||||
let component: AdminContestComponent;
|
||||
let fixture: ComponentFixture<AdminContestComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminContestComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminContestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,39 @@
|
||||
import { Component, ElementRef, Renderer2 } from '@angular/core';
|
||||
import { gotoPage, getEmail } from '../Helper/Helpers';
|
||||
import { Router } from '@angular/router';
|
||||
import { loadPage } from '../Helper/ContestSetup';
|
||||
import { TerminalService } from '../Helper/terminal.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-contest',
|
||||
imports: [],
|
||||
templateUrl: './admin-contest.component.html',
|
||||
styleUrl: './admin-contest.component.css'
|
||||
})
|
||||
export class AdminContestComponent {
|
||||
|
||||
constructor(private renderer: Renderer2, private el: ElementRef, private router: Router, private terminalService: TerminalService){}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
// attach to terminal onload
|
||||
this.terminalService.reattachTerminal();
|
||||
|
||||
loadPage(this.renderer, this.el, this);
|
||||
}
|
||||
|
||||
confirmSelection(){
|
||||
const confirmSelection_ = confirm("You will lose your current flag progress, are you sure?");
|
||||
if(confirmSelection_){
|
||||
gotoPage(this.router, '/admin-profile');
|
||||
}
|
||||
else
|
||||
console.log("Profile navigation canceled");
|
||||
}
|
||||
|
||||
// clear and set terminal for new flag
|
||||
async ClearAndSetTerminal() {
|
||||
await this.terminalService.ClearTerminal();
|
||||
await this.terminalService.reattachTerminal();
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Profile Page on Admin side
|
||||
*/
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: auto auto auto auto auto;
|
||||
grid-template-rows: auto auto auto auto auto auto;
|
||||
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#Header {
|
||||
grid-row: 1/3;
|
||||
grid-column: 1/6;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
.HeadButts {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
margin: 5px;
|
||||
}
|
||||
#ModifyContests {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
#Logout {
|
||||
background-color: red;
|
||||
}
|
||||
#ContestPage {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
|
||||
|
||||
/* Student side of the page */
|
||||
#Students {
|
||||
grid-row: 4/6;
|
||||
grid-column: 1/3;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#Student_Search input, select, button {
|
||||
margin: 5px;
|
||||
}
|
||||
.StudentButts, .FlagButts, select {
|
||||
width: 23%;
|
||||
height: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
#SortButt {
|
||||
width: 10%;
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
#StudentsTable, #FlagsTable {
|
||||
margin: 10px;
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* Flag side of the page */
|
||||
#Flags {
|
||||
grid-row: 4/6;
|
||||
grid-column: 4/6;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#ContestDropDown button, select {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
/* Popup for deleting students */
|
||||
#DialogBox {
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 100px;
|
||||
z-index: 100;
|
||||
}
|
||||
#DialogBox #Delete {
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
#DialogBox button {
|
||||
height: 25px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
/* buttons */
|
||||
#AddStudent {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#EditStudent {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
width: 50%;
|
||||
}
|
||||
#DeleteStudent {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* tooltips */
|
||||
.tooltip {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: inline;
|
||||
}
|
||||
.tooltip:hover .tooltipText {
|
||||
visibility: visible;
|
||||
}
|
||||
.tooltip .tooltipText {
|
||||
visibility: hidden;
|
||||
height: auto;
|
||||
background-color: gray;
|
||||
color: black;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* no ID stuff */
|
||||
table {
|
||||
width: 100%;
|
||||
border: 3px solid black;
|
||||
border-collapse: seperate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
}
|
||||
table thead {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
z-index: 1;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
td {
|
||||
border: 3px solid black;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 80%;
|
||||
height: 50%;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Profile Page for Admins
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title> NMU CTF Profile Page </title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="Header">
|
||||
<h1> Profile </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can do anything and
|
||||
<br>
|
||||
everything with your <span style="color: white">Students</span>
|
||||
or look at the current stats for <span style="color: white;">Flags</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<button class='HeadButts' id="ContestPage" (click)="navtoPageAC()">Back to Contest</button>
|
||||
<button class='HeadButts' id="ModifyContests" (click)="navtoPageMC()">Modify Contests</button>
|
||||
<button class='HeadButts' id="Logout" (click)="onLogout()">Logout</button>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- everything with students side of the page -->
|
||||
<div id="Students">
|
||||
<h1> Students </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
Here you will see a list of your <span style="color:white">Students</span>
|
||||
<br>
|
||||
you can search for specific students
|
||||
<br>
|
||||
or filter them how you would like. For searching a specific <span style="color:white;">Student</span>, make sure
|
||||
<br>
|
||||
<span style="color:white;">Name</span> is selected in the dropdown
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- searching for students -->
|
||||
<div id="Student_Search">
|
||||
<input id="StudentName" type="text" placeholder="Student Name" [(ngModel)]="searchText">
|
||||
<select id="Options">
|
||||
<option disabled selected hidden> Sort By: </option>
|
||||
<option value="Name (A->Z)"> Name (A->Z) </option>
|
||||
<option value="Name (Z->A)"> Name (Z->A) </option>
|
||||
<option value="Flags (H->L)"> Flags (H->L) </option>
|
||||
<option value="Flags (L->H)"> Flags (L->H) </option>
|
||||
</select>
|
||||
<button class="StudentButts" id="SortButt" (click)="sortTable()">Sort</button>
|
||||
<button class="StudentButts" id="AddStudent" (click)="openAddUserPopup()"> Add Student </button>
|
||||
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
<span style="color:lawngreen;">Add Student: </span> opens a popup to add a <span style="color:white;">Student</span> to the list
|
||||
<br>
|
||||
<span style="color:blue;">Edit: </span> allows you to edit a students <span style="color:white;">Password</span>
|
||||
<br>
|
||||
<span style="color:red;">Delete: </span> Deletes a <span style="color:white;">Student</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Student Table -->
|
||||
<div id="StudentsTable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Student Name </th>
|
||||
<th> Flags </th>
|
||||
<th> Email </th>
|
||||
<th> Change Password </th>
|
||||
<th> Delete </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="TBody">
|
||||
<tr *ngFor="let user of users">
|
||||
<td>{{ user.Name }}</td>
|
||||
<td>{{ user.Flags }}</td>
|
||||
<td>{{ user.Email }}</td>
|
||||
<td><button id="EditStudent" (click)="openPasswordPopup(user.Email)">Edit</button></td>
|
||||
<td><button id="DeleteStudent" (click)="openDeleteDialog(user.Email)">Delete</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- everythig with the flags side of the page -->
|
||||
<div id="Flags">
|
||||
<h1> Flags </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can look at how your <span style="color:white;">Students</span>
|
||||
<br>
|
||||
are doing with the <span style="color:white;">Flags</span> you created
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- dropdown for contests -->
|
||||
<div id="ContestDropDown">
|
||||
<select id="DropDown" [(ngModel)]="selectedContest">
|
||||
<option disabled selected hidden> Select a Contest </option>
|
||||
<option *ngFor="let contest of contests" [value] ="contest.Name">{{ contest.Name }}</option>
|
||||
</select>
|
||||
<button class="FlagButts" (click)="populateFlagsTable()">Get Flags</button>
|
||||
<button class="FlagButts" (click)="openLeaderboardPopup(getEmail())">Leaderboard</button>
|
||||
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
<span style="color:white;">Get Flags: </span> shows all the <span style="color:white;">Flags</span> for that contests and the stats
|
||||
<br>
|
||||
<span style="color:white;">Leaderboard: </span> popup a leaderboard for that <span style="color:white;">Contest</span> to see how each <span style="color:white;">Student</span> is doing
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- table for flags -->
|
||||
<div id="FlagsTable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Flag Name </th>
|
||||
<th> Attempts </th>
|
||||
<th> Completions </th>
|
||||
<th> Ratio </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="FlagTBody">
|
||||
<tr *ngFor="let flag of flags">
|
||||
<td>{{ flag.Name }}</td>
|
||||
<td>{{ getAttempts(flag.FlagID )}}</td>
|
||||
<td>{{ getCompletions(flag.FlagID) }}</td>
|
||||
<td [ngStyle]="{ color: getRatioColor(flag.FlagID) }"> {{ getRatio(flag.FlagID) }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dialog box for reassurance of deleting a student -->
|
||||
<dialog id="DialogBox">
|
||||
Are you sure you want to delete <strong id="emailDB">{{ emailToDelete }}</strong>
|
||||
<br>
|
||||
<br>
|
||||
<button (click)="closeDialog()"> Cancel </button>
|
||||
<br>
|
||||
<br>
|
||||
<button id="Delete" (click)="deleteUser()"> Delete </button>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminProfileComponent } from './admin-profile.component';
|
||||
|
||||
describe('AdminProfileComponent', () => {
|
||||
let component: AdminProfileComponent;
|
||||
let fixture: ComponentFixture<AdminProfileComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AdminProfileComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AdminProfileComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,179 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { gotoPage, logOut, getEmail, Popup } from '../Helper/Helpers';
|
||||
import { AdminProfileService } from './admin-profile.service';
|
||||
import { Flag } from '../models/flag.model';
|
||||
import { User } from '../models/user.model';
|
||||
import { Contest } from '../models/contest.model';
|
||||
import { Submission } from '../models/submission.model';
|
||||
import { NgFor } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-profile',
|
||||
imports: [NgFor, FormsModule, CommonModule],
|
||||
templateUrl: './admin-profile.component.html',
|
||||
styleUrl: './admin-profile.component.css'
|
||||
})
|
||||
export class AdminProfileComponent implements OnInit{
|
||||
users: User[] = []
|
||||
displayedUsers: User[] = []
|
||||
contests: Contest[] = []
|
||||
subs: Submission[] = []
|
||||
flags: Flag[] = []
|
||||
selectedContest: string = '';
|
||||
sortOption: string = '';
|
||||
constructor(private router: Router, private adminProfileService: AdminProfileService){}
|
||||
emailToDelete = '';
|
||||
searchText: string = '';
|
||||
|
||||
ngOnInit(){
|
||||
this.getContests();
|
||||
this.populateTable();
|
||||
}
|
||||
|
||||
|
||||
async populateTable(){
|
||||
try{
|
||||
const email = this.getEmail();
|
||||
const users = await this.adminProfileService.getAllUsers(email);
|
||||
this.users = users;
|
||||
} catch(error) {
|
||||
console.error('Error populating user table', error);
|
||||
}
|
||||
}
|
||||
|
||||
async populateFlagsTable(): Promise<void> {
|
||||
if(!this.selectedContest){
|
||||
alert('Must select a contest');
|
||||
return;
|
||||
}
|
||||
try{
|
||||
const email = getEmail();
|
||||
const result = await this.adminProfileService.getContestFlagsSubs(email, this.selectedContest);
|
||||
this.flags = result.flags;
|
||||
this.subs = result.subs;
|
||||
}catch(error){
|
||||
console.error('Failed to populate flag table');
|
||||
}
|
||||
}
|
||||
|
||||
getAttempts(flagId: number): number{
|
||||
return this.subs.filter(sub => sub.FlagID === flagId).reduce((acc, sub) => acc + sub.Attempts, 0);
|
||||
}
|
||||
|
||||
getCompletions(flagId: number): number{
|
||||
return this.subs.filter(sub => sub.FlagID === flagId).reduce((acc, sub) => acc + (sub.IsCorrect ? 1 : 0), 0);
|
||||
}
|
||||
|
||||
getRatio(flagId: number): number{
|
||||
const attempts = this.getAttempts(flagId);
|
||||
const completions = this.getCompletions(flagId);
|
||||
return attempts > 0 ? +(completions / attempts * 100).toFixed(2) : 0;
|
||||
}
|
||||
|
||||
getRatioColor(flagId: number): string{
|
||||
const ratio = this.getRatio(flagId);
|
||||
if(ratio >= 80) return 'lawngreen';
|
||||
else if (ratio >= 60) return 'orange';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
openPasswordPopup(email: string){
|
||||
sessionStorage.setItem('selectedUser', email);
|
||||
Popup('/edit-student');
|
||||
}
|
||||
|
||||
openAddUserPopup(){
|
||||
Popup('/add-student');
|
||||
}
|
||||
|
||||
openLeaderboardPopup(email: string){
|
||||
Popup('Leaderboard.html');
|
||||
}
|
||||
|
||||
async getContests(){
|
||||
try {
|
||||
const email = this.getEmail();
|
||||
const contests = await this.adminProfileService.getContests(email);
|
||||
this.contests = contests;
|
||||
} catch(error){
|
||||
console.error("Error getting contests", error);
|
||||
}
|
||||
}
|
||||
|
||||
openDeleteDialog(email: string): void {
|
||||
this.emailToDelete = email;
|
||||
const dialog = document.getElementById('DialogBox') as HTMLDialogElement
|
||||
if(dialog){
|
||||
dialog.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser(){
|
||||
try{
|
||||
if(!this.emailToDelete) return;
|
||||
await this.adminProfileService.deleteUser(this.emailToDelete);
|
||||
console.log("User deleted successfully");
|
||||
this.closeDialog();
|
||||
this.populateTable();
|
||||
} catch(error) {
|
||||
console.error("Error deleting user", error);
|
||||
}
|
||||
}
|
||||
|
||||
closeDialog(): void {
|
||||
const dialog = document.getElementById('DialogBox') as HTMLDialogElement;
|
||||
if(dialog)
|
||||
dialog.close();
|
||||
this.emailToDelete = '';
|
||||
}
|
||||
|
||||
sortTable(): void{
|
||||
const name = this.searchText.trim().toLowerCase();
|
||||
const option = this.sortOption;
|
||||
let sorted = [...this.users];
|
||||
|
||||
if((option === 'Name (A->Z)' || option === 'Name (Z->A)') && name !== ''){
|
||||
const targetIndex = sorted.findIndex(s => s.Name.toLowerCase() === name);
|
||||
if(targetIndex !== -1){
|
||||
const student = sorted.splice(targetIndex, 1)[0];
|
||||
sorted.unshift(student);
|
||||
}
|
||||
else{
|
||||
alert('Student is not in database');
|
||||
}
|
||||
}
|
||||
|
||||
if(option === 'Name (A->Z)'){
|
||||
sorted.sort((a, b) => a.Name.localeCompare(b.Name));
|
||||
}
|
||||
else if(option === 'Name (Z->A)'){
|
||||
sorted.sort((a, b) => b.Name.localeCompare(a.Name));
|
||||
}
|
||||
else if(option === 'Flags (H->L)'){
|
||||
sorted.sort((a, b) => b.Flags - a.Flags);
|
||||
}
|
||||
else if(option === 'Flags (L->H)'){
|
||||
sorted.sort((b, a) => a.Flags - b.Flags);
|
||||
}
|
||||
this.displayedUsers = sorted;
|
||||
}
|
||||
|
||||
navtoPageAC(): void{
|
||||
gotoPage(this.router, '/admin-contest');
|
||||
}
|
||||
|
||||
navtoPageMC(): void{
|
||||
gotoPage(this.router, '/modify-contest');
|
||||
}
|
||||
|
||||
onLogout(): void{
|
||||
logOut(this.router);
|
||||
}
|
||||
|
||||
getEmail(): string{
|
||||
return getEmail();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdminProfileService } from './admin-profile.service';
|
||||
|
||||
describe('AdminProfileService', () => {
|
||||
let service: AdminProfileService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AdminProfileService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
|
||||
export class AdminProfileService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
async getAllUsers(email: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/users/getAllUsers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch users');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser(email: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/users/DeleteStudent', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete user');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContests(email: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/contests/getContests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch contests');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching contests:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getContestFlagsSubs(email: string, contest: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/getContestFlagsSubs', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email, contest })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch contest flags');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching contest flags:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
<router-outlet></router-outlet>
|
@ -0,0 +1,29 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [AppComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have the 'CTF-Frontend' title`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('CTF-Frontend');
|
||||
});
|
||||
|
||||
it('should render title', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, CTF-Frontend');
|
||||
});
|
||||
});
|
@ -0,0 +1,14 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.css'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'CTF-Frontend';
|
||||
|
||||
constructor(private router: Router){}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
|
||||
};
|
@ -0,0 +1,131 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { AdminGuard } from './guards/AdminGuard.guard';
|
||||
import { UserGuard } from './guards/UserGuard.guard';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { UserMenuComponent } from './user-menu/user-menu.component';
|
||||
import { UserProfileComponent } from './user-profile/user-profile.component';
|
||||
import { ContestPageComponent } from './contest-page/contest-page.component';
|
||||
import { UserProfileACComponent } from './user-profile-ac/user-profile-ac.component';
|
||||
import { AdminContestComponent } from './admin-contest/admin-contest.component';
|
||||
import { AdminProfileComponent } from './admin-profile/admin-profile.component';
|
||||
import { LeaderboardComponent } from './leaderboard/leaderboard.component';
|
||||
import { ModifyContestComponent } from './modify-contest/modify-contest.component';
|
||||
import { AddContestComponent } from './add-contest/add-contest.component';
|
||||
import { AddStudentComponent } from './add-student/add-student.component';
|
||||
import { EditStudentComponent } from './edit-student/edit-student.component';
|
||||
import { AddFlagComponent } from './add-flag/add-flag.component';
|
||||
import { UNAUTHORIZEDComponent } from './unauthorized/unauthorized.component';
|
||||
import { CreateImageComponent } from './create-image/create-image.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path:'',
|
||||
component: LoginComponent,
|
||||
title: 'Login',
|
||||
},
|
||||
|
||||
{
|
||||
path: 'user-menu',
|
||||
component: UserMenuComponent,
|
||||
title: 'User Menu',
|
||||
canActivate: [AuthGuard, UserGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'user-profile',
|
||||
component: UserProfileComponent,
|
||||
title: 'User Profile',
|
||||
canActivate: [AuthGuard, UserGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'contest-page',
|
||||
component: ContestPageComponent,
|
||||
title: 'Contest Page',
|
||||
canActivate: [AuthGuard, UserGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'user-profileAC',
|
||||
component: UserProfileACComponent,
|
||||
title: 'User Profile',
|
||||
canActivate: [AuthGuard, UserGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'admin-contest',
|
||||
component: AdminContestComponent,
|
||||
title: 'Admin Contest',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'admin-profile',
|
||||
component: AdminProfileComponent,
|
||||
title: 'Admin Profile',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'leaderboard',
|
||||
component: LeaderboardComponent,
|
||||
title: 'Leaderboard'
|
||||
},
|
||||
|
||||
{
|
||||
path: 'modify-contest',
|
||||
component: ModifyContestComponent,
|
||||
title: 'Modify Contest',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'add-contest',
|
||||
component: AddContestComponent,
|
||||
title: 'Add Contest',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'add-student',
|
||||
component: AddStudentComponent,
|
||||
title: 'Add Student',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'edit-student',
|
||||
component: EditStudentComponent,
|
||||
title: 'Edit Student',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'add-flag',
|
||||
component: AddFlagComponent,
|
||||
title: 'Add Flag',
|
||||
canActivate: [AuthGuard, AdminGuard]
|
||||
},
|
||||
|
||||
{
|
||||
path: 'unauthorized',
|
||||
component: UNAUTHORIZEDComponent,
|
||||
title: 'You think youre slick eh?'
|
||||
},
|
||||
|
||||
{
|
||||
path: 'create-image',
|
||||
component: CreateImageComponent,
|
||||
title: 'Create Image'
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forRoot(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
|
||||
export class AppRoutingModule{}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
Jordan Latimer
|
||||
Alex Miller
|
||||
|
||||
CSS for both Contest Pages
|
||||
*/
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
background-color: green;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: 15% 10% auto 20%;
|
||||
grid-template-rows: 10% 40% 10% auto;
|
||||
}
|
||||
|
||||
/* Name of the contest at the top left */
|
||||
#Contest_Name {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/3;
|
||||
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 175%;
|
||||
}
|
||||
#Contest_Name * {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* sidebar thats scrollable for different flags */
|
||||
#Flag_Sidebar {
|
||||
background-color: white;
|
||||
grid-row: 2/5;
|
||||
grid-column: 1/2;
|
||||
height: 100%;
|
||||
}
|
||||
#Flag_Sidebar ul {
|
||||
list-style-type: none;
|
||||
height: 80%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
background-color: white;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* details for each flag (name, description, etc) */
|
||||
#Flag_Deats {
|
||||
background-color: white;
|
||||
grid-row: 2/3;
|
||||
grid-column: 2/4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#notificationMessage {
|
||||
background-color: rgba(65, 145, 55, 0.511);
|
||||
margin-left: 50%;
|
||||
grid-row: 3/4;
|
||||
grid-column: 3/5;
|
||||
}
|
||||
|
||||
.notificationMessage {
|
||||
background-color: rgba(65, 145, 55, 0.511);
|
||||
margin-left: 50%;
|
||||
grid-row: 3/4;
|
||||
grid-column: 3/5;
|
||||
}
|
||||
|
||||
#Notifications.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.notification-banner {
|
||||
background-color: rgba(65, 145, 55, 0.511);
|
||||
margin-left: 50%;
|
||||
grid-row: 3/4;
|
||||
grid-column: 3/5;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Submitting the flag */
|
||||
#Submit_Flag {
|
||||
background-color: white;
|
||||
grid-row: 3/4;
|
||||
grid-column: 2/4;
|
||||
|
||||
}
|
||||
#Submit_Flag_Stuff * {
|
||||
height: 40%;
|
||||
width: 15%;
|
||||
}
|
||||
#Submit_Butt {
|
||||
background-color: lawngreen;
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
}
|
||||
#Submit_Flag h3 {
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
display: inline;
|
||||
}
|
||||
.Incorrect, .Correct {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
font-size: 125%;
|
||||
}
|
||||
.Incorrect {
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
.Correct {
|
||||
background-color: lawngreen;
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* hints for each flag */
|
||||
#Hints {
|
||||
background-color: white;
|
||||
grid-row: 2/4;
|
||||
grid-column: 4/5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
textarea {
|
||||
background-color: white;
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Linux Shell for commands */
|
||||
#Linux_Shell {
|
||||
grid-row: 4/5;
|
||||
grid-column: 2/5;
|
||||
color: lawngreen;
|
||||
}
|
||||
|
||||
/* when a flag is selected */
|
||||
.selected-flag {
|
||||
background-color: lightgray;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Flags and Hints Labels */
|
||||
.FlagHint {
|
||||
background-color: black;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
font-size: 200%;
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Contest Page for Users
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title> NMU CTF Contest Page </title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Bar at top for moving tabs, this is for the practice contest -->
|
||||
<ng-container *ngIf="isPractice">
|
||||
<a href="#" (click)="confirmSelection(1); $event.preventDefault()">Leave Practice</a>
|
||||
</ng-container>
|
||||
|
||||
<!-- Show if NOT Admin and NOT in Practice -->
|
||||
<ng-container id="Taskbar" *ngIf="!isPractice && !isAdmin">
|
||||
<a id="Current_Page" href="#" (click)="navtoPageUM(); $event.preventDefault()">Contest</a>
|
||||
<a href="#" (click)="confirmSelection(2); $event.preventDefault()">Profile</a>
|
||||
</ng-container>
|
||||
<!-- Contest name -->
|
||||
<div id="Contest_Name">
|
||||
<h1 id="ContHeader"></h1>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar thats scrollable for different flags -->
|
||||
<div id="Flag_Sidebar">
|
||||
<h2 class="FlagHint"> Flags </h2>
|
||||
<ul id="FlagList">
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- details for each flag (name, description etc) -->
|
||||
<div id="Flag_Deats">
|
||||
<div id ="DefaultMessage">
|
||||
<h2>Welcome to NMU CTF</h2>
|
||||
<p>There are currently no contests active at this time</p>
|
||||
</div>
|
||||
<h2 id="ContName"></h2>
|
||||
<p id="ContDesc"></p>
|
||||
<h2 id="Flag_Name"></h2>
|
||||
<p id="Desc"></p>
|
||||
</div>
|
||||
|
||||
<!-- Submitting the flag -->
|
||||
<div id="Submit_Flag">
|
||||
<div id="Submit_Flag_Stuff">
|
||||
<h3> Submit Flag: </h3>
|
||||
<input type="text" placeholder="NMUCTF${FLAG}">
|
||||
<input id="Submit_Butt" type="submit" value="Submit" (click)="onSubmission()">
|
||||
<p id="Submission_Result"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="notificationMessage" id="notificationMessage" class="notificationMessage">
|
||||
{{ notificationMessage }}
|
||||
</div>
|
||||
<!-- Hints for each flag -->
|
||||
<div id="Hints">
|
||||
<div id="Hint_Stuff">
|
||||
<h2 class="FlagHint"> Hints </h2>
|
||||
<div id="Hint_Butts">
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<textarea readonly rows="15" cols="30" id="Hint_Desc"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Linux Shell for commands -->
|
||||
<div id="Linux_Shell"></div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ContestPageComponent } from './contest-page.component';
|
||||
|
||||
describe('ContestPageComponent', () => {
|
||||
let component: ContestPageComponent;
|
||||
let fixture: ComponentFixture<ContestPageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ContestPageComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ContestPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,88 @@
|
||||
import { Component, ElementRef, OnInit, Renderer2 } from '@angular/core';
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Router } from '@angular/router'
|
||||
import { loadPage, handleSubmission, getAdmin } from '../Helper/ContestSetup';
|
||||
import { getEmail, gotoPage } from '../Helper/Helpers';
|
||||
import { TerminalService } from '../Helper/terminal.service';
|
||||
import { SocketIOService } from '../notifications/socket-io.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-contest-page',
|
||||
imports: [NgIf],
|
||||
templateUrl: './contest-page.component.html',
|
||||
styleUrl: './contest-page.component.css'
|
||||
})
|
||||
|
||||
export class ContestPageComponent implements OnInit {
|
||||
isPractice = window.location.href.includes("Prac");
|
||||
isAdmin: boolean = this.isItAdmin();
|
||||
notificationMessage: string | null = null;
|
||||
|
||||
constructor(private renderer: Renderer2,
|
||||
private el: ElementRef,
|
||||
private router: Router,
|
||||
private terminalService: TerminalService,
|
||||
private socketService: SocketIOService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// attach to terminal onload
|
||||
this.terminalService.reattachTerminal();
|
||||
//this.socketService.connect();
|
||||
loadPage(this.renderer, this.el, this);
|
||||
/*this.socketService.listen('submission-notification').subscribe((data: any) => {
|
||||
console.log('Received notification:', data);
|
||||
this.notificationMessage = `${data.email} solved ${data.flagName}!`;
|
||||
console.log('Updated notif:', this.notificationMessage);
|
||||
setTimeout(() => {
|
||||
this.notificationMessage = null;
|
||||
}, 5000);
|
||||
});*/
|
||||
|
||||
this.socketService.listen('test-event').subscribe((data: any) => {
|
||||
console.log('Test event received:', data);
|
||||
this.notificationMessage = 'Test event received';
|
||||
});
|
||||
this.socketService.emit('test-event', { message: 'Hello from client' });
|
||||
}
|
||||
|
||||
// clear and set terminal for new flag
|
||||
async ClearAndSetTerminal() {
|
||||
await this.terminalService.ClearTerminal();
|
||||
await this.terminalService.reattachTerminal();
|
||||
}
|
||||
|
||||
onSubmission(): void{
|
||||
handleSubmission(this.el, this.socketService);
|
||||
}
|
||||
|
||||
confirmSelection(num: number){
|
||||
const confirmSelection_ = confirm("You will lose your current flag progress, are you sure?");
|
||||
if(confirmSelection_){
|
||||
const email = getEmail();
|
||||
if(num === 1)
|
||||
gotoPage(this.router, '/user-menu');
|
||||
else
|
||||
gotoPage(this.router, '/user-profileAC');
|
||||
}
|
||||
else
|
||||
console.log("Profile navigation canceled");
|
||||
}
|
||||
|
||||
navtoPageA(): void {
|
||||
gotoPage(this.router, '/contest-page');
|
||||
}
|
||||
|
||||
navtoPageUM(): void{
|
||||
gotoPage(this.router, '/user-menu')
|
||||
}
|
||||
|
||||
isItAdmin(): boolean{
|
||||
if(getAdmin() === "True")
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Create image
|
||||
*/
|
||||
|
||||
#body {
|
||||
display: grid;
|
||||
grid-template-columns: 30% 30% auto;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
background-color: lightgreen;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#HeaderDiv {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/4;
|
||||
background-color: green;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* form stuff like image name, file name, add file etc */
|
||||
#FormDiv {
|
||||
grid-row: 2/3;
|
||||
grid-column: 1/4;
|
||||
background-color: green;
|
||||
margin: 10px;
|
||||
}
|
||||
#FormStuff {
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Contents of the File */
|
||||
#FileContentsDiv {
|
||||
grid-row: 3/6;
|
||||
grid-column: 1/2;
|
||||
background-color: green;
|
||||
margin: 10px;
|
||||
}
|
||||
#FileContents {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* drop box to drop files */
|
||||
#DropBoxDiv {
|
||||
grid-row: 3/6;
|
||||
grid-column: 2/3;
|
||||
background-color: green;
|
||||
margin: 10px;
|
||||
}
|
||||
#DropBox {
|
||||
border: 3px black dashed;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
background-color: white;
|
||||
margin: 10px;
|
||||
height: 310px;
|
||||
}
|
||||
|
||||
/* file tree visual */
|
||||
#FileTreeDiv {
|
||||
grid-row: 3/6;
|
||||
grid-column: 3/4;
|
||||
background-color: green;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
#FileTree {
|
||||
background-color: white;
|
||||
margin: 10px;
|
||||
height: 310px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
.Green {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
.Blue {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
height: 25%;
|
||||
}
|
||||
button {
|
||||
font-weight: bold;
|
||||
width: 10%;
|
||||
height: 25%;
|
||||
margin: 10px;
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Page for creating images for flags
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title> Create Image </title>
|
||||
</head>
|
||||
<body id="body">
|
||||
|
||||
<!-- Header -->
|
||||
<div id="HeaderDiv">
|
||||
<h1> Create Image </h1>
|
||||
<button class="Blue" (click)="navtoPageMC()">Back to Modify</button>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
|
||||
<!-- File Contents -->
|
||||
<div id="FileContentsDiv">
|
||||
<h1> Contents of File </h1>
|
||||
<div id="FileContents">
|
||||
<div id="TextBox">
|
||||
<textarea id="filecontents" placeholder="Enter contents of files here" cols="40" rows="20"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form for adding files and directories -->
|
||||
<div id="FormDiv">
|
||||
<h1> File and Image </h1>
|
||||
<div id="FormStuff">
|
||||
<label> Image Name: </label>
|
||||
<input id="ImageName" type="text" placeholder="image name">
|
||||
<br>
|
||||
<br>
|
||||
<label> Name: </label>
|
||||
<input id="filename" type="text" placeholder="Folder or File name">
|
||||
<br>
|
||||
<br>
|
||||
<button class="Green" (click)="getFile(true)"> Add Folder</button>
|
||||
<button class="Green" (click)="getFile(false)"> Add File </button>
|
||||
<button (click)="DeleteFile()" style="background-color: red;"> Delete </button>
|
||||
<br>
|
||||
<br>
|
||||
<button class="Green" (click)="CreateImage()" type="submit">Create Image</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Drop Box -->
|
||||
<div id="DropBoxDiv">
|
||||
<h1> Drop Box </h1>
|
||||
<div id="DropBox">
|
||||
<p> Drag and Drop files here or: </p>
|
||||
<input type="file" id="FileInput" multiple (change)="onFileChange($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Tree -->
|
||||
<div id="FileTreeDiv">
|
||||
<h1> File Tree </h1>
|
||||
<div id="FileTree">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateImageComponent } from './create-image.component';
|
||||
|
||||
describe('CreateImageComponent', () => {
|
||||
let component: CreateImageComponent;
|
||||
let fixture: ComponentFixture<CreateImageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CreateImageComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateImageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,314 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TreeService } from '../Helper/tree.service';
|
||||
import { getEmail, gotoPage } from '../Helper/Helpers';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-image',
|
||||
imports: [],
|
||||
templateUrl: './create-image.component.html',
|
||||
styleUrl: './create-image.component.css'
|
||||
})
|
||||
export class CreateImageComponent {
|
||||
|
||||
private root: TreeService = new TreeService("Root", "", true, 1, null);
|
||||
private allFiles: File[] | null = null;
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
// initialize
|
||||
ngOnInit() : void {
|
||||
|
||||
// create the root and refresh the file tree
|
||||
this.RefreshFileTree();
|
||||
}
|
||||
|
||||
RefreshFileTree() {
|
||||
|
||||
const filetree = document.getElementById('FileTree') as HTMLUListElement;
|
||||
filetree.innerHTML = '';
|
||||
|
||||
// create list and add the root
|
||||
const list = document.createElement('ul');
|
||||
|
||||
// go through full tree
|
||||
const fulllist = this.AddListItem(list, this.root);
|
||||
filetree.appendChild(fulllist);
|
||||
|
||||
this.ClearInputs();
|
||||
}
|
||||
|
||||
// uploading files through dropbox
|
||||
onFileChange(event: any) {
|
||||
const files: FileList = event.target.files;
|
||||
if (files.length > 0) {
|
||||
if (!this.allFiles) this.allFiles = []; // set the array up if it's null
|
||||
for (let i=0; i < files.length; i++) {
|
||||
this.allFiles?.push(files[i]);
|
||||
|
||||
// add to tree
|
||||
if (this.AddFileToTree(files[i], false, "", "")) return;
|
||||
}
|
||||
// reset files inside dropbox?
|
||||
this.RefreshFileTree();
|
||||
this.ClearInputs();
|
||||
}
|
||||
}
|
||||
|
||||
// add the file to the tree
|
||||
AddFileToTree(file: File | null, dir: boolean, filename: string, contents: string) : boolean{
|
||||
const elements = document.querySelectorAll('.selected-node');
|
||||
|
||||
// make sure a parent node is selected
|
||||
if (elements.length == 0) {
|
||||
alert('Must select a Directory');
|
||||
this.ClearInputs();
|
||||
return true;
|
||||
}
|
||||
|
||||
// make sure all the nodes selected are directories
|
||||
for (let i=0; i < elements.length; i++) {
|
||||
let li = elements[i] as HTMLLIElement;
|
||||
if (li.firstChild?.textContent?.substring(0,4) !== "(D)-") {
|
||||
alert('Must select a directory');
|
||||
this.ClearInputs();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// loop through all the elements adding the name of the file to the tree
|
||||
for (let i=0; i < elements.length; i++) {
|
||||
|
||||
// get the parent node selected
|
||||
let li = elements[i] as HTMLLIElement;
|
||||
let results = this.getNodeNumFromClass(li);
|
||||
let parentname: string = (results[0]).toString();
|
||||
let parentnum: number = Number(results[1]);
|
||||
let node = this.root.getNodeByName(parentname,parentnum);
|
||||
|
||||
// if the parent node isnt a directory, dont add it
|
||||
if (node?.getDir() == false) {
|
||||
alert('can only add files/folders to directories');
|
||||
this.ClearInputs();
|
||||
return true;
|
||||
}
|
||||
|
||||
// make sure the filename isnt taken already
|
||||
if (!node?.IsNameTaken(filename)) {
|
||||
|
||||
// get the number of nodes of this name for nodenum
|
||||
let nodes: TreeService[] = [];
|
||||
nodes = this.root.getNodesSameName(filename, nodes);
|
||||
let nodenum = nodes.length + 1;
|
||||
|
||||
// add the node to the tree
|
||||
if (file == null) { // not using dropbox
|
||||
let newFile = new TreeService(filename, contents, dir, nodenum, node);
|
||||
node?.AddChild(newFile);
|
||||
}
|
||||
else { // using dropbox
|
||||
let FileObjName : string = file?.name!;
|
||||
let newFile = new TreeService(FileObjName, "", false, nodenum, node);
|
||||
node?.AddChild(newFile);
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
alert('That file name is already under this directory');
|
||||
this.ClearInputs();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// get the file stuff through creating a new file
|
||||
getFile(dir: boolean) {
|
||||
let nameElement = document.getElementById('filename') as HTMLInputElement;
|
||||
let filename: string = nameElement.value;
|
||||
let contentsElement = document.getElementById('filecontents') as HTMLTextAreaElement;
|
||||
let contents : string = contentsElement.value;
|
||||
|
||||
if(this.AddFileToTree(null, dir, filename, contents)) return;
|
||||
this.RefreshFileTree();
|
||||
}
|
||||
|
||||
|
||||
// use recursion to go through the tree adding each child
|
||||
AddListItem(list: HTMLUListElement, node: TreeService) {
|
||||
|
||||
// add node to current list
|
||||
const nodeitem = document.createElement('li');
|
||||
const spanitem = document.createElement('span');
|
||||
|
||||
if (node.getDir()) spanitem.textContent = '(D)-' + node.getName();
|
||||
else spanitem.textContent = node.getName();
|
||||
|
||||
// add the name and nodenum to the class list
|
||||
spanitem.classList.add(node.getName() + node.getNodeNum());
|
||||
|
||||
spanitem.addEventListener('click', function(nodeitem) {
|
||||
const listitem = nodeitem.target as HTMLLIElement;
|
||||
listitem?.classList.toggle('selected-node');
|
||||
});
|
||||
|
||||
nodeitem.appendChild(spanitem);
|
||||
// add to list
|
||||
list.appendChild(nodeitem);
|
||||
|
||||
// if node has children
|
||||
if (node.getChildren().length > 0) {
|
||||
let nodechildren = node.getChildren();
|
||||
const childlist = document.createElement('ul');
|
||||
for (var i=0; i < nodechildren.length; i++) {
|
||||
let child = nodechildren[i];
|
||||
list.appendChild(this.AddListItem(childlist, child));
|
||||
}
|
||||
}
|
||||
|
||||
// return the whole list
|
||||
return list;
|
||||
}
|
||||
|
||||
// get nodenum from class list
|
||||
getNodeNumFromClass(item : HTMLLIElement) {
|
||||
|
||||
// get the class list of the selected item
|
||||
const WholeClassList = item.classList;
|
||||
const namenum = WholeClassList[0];
|
||||
|
||||
// break up the string into the two parts
|
||||
const index = namenum.search(/\d/);
|
||||
const name = namenum.substring(0,index);
|
||||
let num : number = Number(namenum.substring(index));
|
||||
|
||||
const results = [name,num];
|
||||
return results;
|
||||
}
|
||||
|
||||
// delete a file from the tree
|
||||
DeleteFile() {
|
||||
|
||||
const items = document.querySelectorAll('.selected-node');
|
||||
|
||||
// go through each item selected and remove it from parent
|
||||
for (var i=0; i < items.length; i++) {
|
||||
|
||||
let li = items[i] as HTMLLIElement;
|
||||
|
||||
// get the name and num from class list
|
||||
let results = this.getNodeNumFromClass(li);
|
||||
let name : string = results[0].toString();
|
||||
let num : number = Number(results[1]);
|
||||
|
||||
let node = this.root.getNodeByName(name,num);
|
||||
|
||||
// if root then delete everything
|
||||
if (node?.getName() === 'Root') {
|
||||
this.root.DeleteTheChildren();
|
||||
this.RefreshFileTree();
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the correct nodes
|
||||
const parent = node?.getParent();
|
||||
const newchildren = parent?.getChildren().filter(child => child.getName() !== node?.getName());
|
||||
parent?.setChildren(newchildren!);
|
||||
}
|
||||
|
||||
// refresh the tree visual
|
||||
this.RefreshFileTree();
|
||||
}
|
||||
|
||||
// Create the actual Image
|
||||
CreateImage() {
|
||||
|
||||
const imagename = this.getImageName();
|
||||
if (imagename === true) return;
|
||||
|
||||
this.root.DeleteParentRef();
|
||||
this.SendTree(imagename);
|
||||
|
||||
}
|
||||
|
||||
// get the name of the image and add the time to it
|
||||
getImageName() {
|
||||
const imgElement = document.getElementById('ImageName') as HTMLInputElement;
|
||||
const imgname = imgElement.value;
|
||||
|
||||
// make sure name is included
|
||||
if (imgname === undefined || imgname === null || imgname === '') {
|
||||
alert('Must include a name for the image');
|
||||
return true;
|
||||
}
|
||||
|
||||
// docker states all image names must be lowercase, so have to check for that
|
||||
if (/[A-Z]/.test(imgname) === true) {
|
||||
alert('Image name must be all lowercase');
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the current time to add to the name so no images have same name
|
||||
let time = new Date().toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
// get rid of the colons and add the time to the image name
|
||||
time = time.replace(/:/g,'');
|
||||
return imgname + time;
|
||||
}
|
||||
|
||||
// send the full tree structure to the server side
|
||||
async SendTree(imgname : string) {
|
||||
|
||||
const data = {
|
||||
root: this.root, imgname: imgname, email: getEmail()
|
||||
};
|
||||
|
||||
// create a form data to hold both JSON and the files
|
||||
const formdata = new FormData();
|
||||
|
||||
// append each file to the form data with the key files
|
||||
this.allFiles?.forEach((file : File) => {
|
||||
formdata.append("files", file, file.name);
|
||||
console.log(file.name);
|
||||
});
|
||||
|
||||
formdata.append('data', JSON.stringify(data));
|
||||
|
||||
// send the post request
|
||||
const res = await fetch('api/AddImage', {
|
||||
method: 'POST',
|
||||
body: formdata
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
// go back to modify contest
|
||||
}
|
||||
}
|
||||
|
||||
// clear the input fields after each addition/error
|
||||
ClearInputs() {
|
||||
|
||||
// contents of file
|
||||
let contentElement = document.getElementById('filecontents') as HTMLTextAreaElement;
|
||||
contentElement.value = '';
|
||||
|
||||
// name of file
|
||||
let filenameElement = document.getElementById('filename') as HTMLInputElement;
|
||||
filenameElement.value = '';
|
||||
|
||||
// file input box
|
||||
let fileinputElement = document.getElementById('FileInput') as HTMLInputElement;
|
||||
fileinputElement.value = '';
|
||||
}
|
||||
|
||||
// go back to Modify Contest
|
||||
navtoPageMC() {
|
||||
gotoPage(this.router, '/modify-contest');
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
html {
|
||||
background-color: green;
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
form {
|
||||
text-align: center;
|
||||
}
|
||||
h1, h3 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
#Inputs {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content;
|
||||
grid-gap: 5px;
|
||||
margin-left: 25%;
|
||||
margin-top: 25%;
|
||||
}
|
||||
label {
|
||||
text-align: right;
|
||||
}
|
||||
#Submit {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
#submit * {
|
||||
width: 15%;
|
||||
height: 5%;
|
||||
}
|
||||
#Submit button {
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#hint1Text, #hint2Text, #hint3Text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show the respective hint fields based on the selected radio button */
|
||||
#hint1:checked ~ #hint1Text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#hint2:checked ~ #hint1Text,
|
||||
#hint2:checked ~ #hint2Text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#hint3:checked ~ #hint1Text,
|
||||
#hint3:checked ~ #hint2Text,
|
||||
#hint3:checked ~ #hint3Text{
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* for Create Image */
|
||||
#body {
|
||||
display: grid;
|
||||
grid-template-rows: 25% auto auto auto auto auto 25%;
|
||||
grid-template-columns: 20% 20% 20% 20% 20%;
|
||||
justify-items: center;
|
||||
}
|
||||
#labels {
|
||||
grid-row: 1/2;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
#Butts {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
align-self: flex-end;
|
||||
}
|
||||
#TextBox {
|
||||
grid-row: 3/6;
|
||||
grid-column: 1/3;
|
||||
}
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
#DropBox {
|
||||
grid-row: 3/6;
|
||||
grid-column: 3/4;
|
||||
border: 3px black dashed;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
}
|
||||
#SendFile {
|
||||
grid-row: 6/7;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
#FileTree {
|
||||
grid-row: 3/6;
|
||||
grid-column: 4/6;
|
||||
}
|
||||
#Submition {
|
||||
grid-row: 7/8;
|
||||
grid-column: 3/4;
|
||||
}
|
||||
.selected-node {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Changing passwords of users for Admin
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<title> Change Password </title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- everything needed for adding a student -->
|
||||
<h1> Change Password </h1>
|
||||
<h3 id="Email">Email: {{emailinput}}</h3>
|
||||
<form id="Inputs">
|
||||
<label> Change Password: </label>
|
||||
<input *ngIf="!showPass" id="PS1" type="password" [(ngModel)]="newPassword" name="newPassword" align="middle">
|
||||
<input *ngIf="showPass" id="PS1" type="text" [(ngModel)]="newPassword" name="newPassword" align="middle">
|
||||
<label> Confirm Password: </label>
|
||||
<input *ngIf="!showPass" id="PS2" type="password" [(ngModel)]="confirmPassword" name="confirmPassword" align="middle">
|
||||
<input *ngIf="showPass" id="PS2" type="text" [(ngModel)]="confirmPassword" name="confirmPassword" align="middle">
|
||||
<label> Show Password: </label>
|
||||
<input type='checkbox' (click)='ShowPassword()' id='ShowPass'>
|
||||
</form>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- submitting or closing -->
|
||||
<div id="Submit">
|
||||
<input id="EditPass" type="submit" (click)="EditPass();">
|
||||
<button onclick="self.close();"> Cancel </button>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { EditStudentComponent } from './edit-student.component';
|
||||
|
||||
describe('EditStudentComponent', () => {
|
||||
let component: EditStudentComponent;
|
||||
let fixture: ComponentFixture<EditStudentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EditStudentComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EditStudentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,51 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-student',
|
||||
imports: [FormsModule, NgIf],
|
||||
templateUrl: './edit-student.component.html',
|
||||
styleUrl: './edit-student.component.css'
|
||||
})
|
||||
export class EditStudentComponent {
|
||||
emailinput: string | null = sessionStorage.getItem('selectedUser');;
|
||||
showPass: boolean = false;
|
||||
newPassword: string = '';
|
||||
confirmPassword: string = '';
|
||||
// edit the password of a student
|
||||
async EditPass() {
|
||||
const ps1 = this.newPassword;
|
||||
const ps2 = this.confirmPassword
|
||||
// password protection
|
||||
if (ps1.length < 10 || ps1 == "") {
|
||||
alert("password must be at least 10 characters");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ps1 == ps2) { // if both passwords are equal, continue
|
||||
const data = { email: this.emailinput, password: ps1};
|
||||
const res = await fetch('api/UpdateStudent', {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
console.log("Success: changing password");
|
||||
window.close();
|
||||
}
|
||||
else {
|
||||
console.log("ERROR: changing password");
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert('Passwords do not match');
|
||||
}
|
||||
}
|
||||
|
||||
ShowPassword(){
|
||||
this.showPass = !this.showPass;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminGuard implements CanActivate {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
const role = sessionStorage.getItem('role');
|
||||
if (role === 'admin') return true;
|
||||
|
||||
this.router.navigate(['/unauthorized']);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserGuard implements CanActivate {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
const role = sessionStorage.getItem('role');
|
||||
if (role === 'user') return true;
|
||||
|
||||
this.router.navigate(['/unauthorized']);
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CanActivate, Router } from '@angular/router';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private router: Router) {}
|
||||
|
||||
canActivate(): boolean {
|
||||
const email = sessionStorage.getItem('email');
|
||||
if (!email) {
|
||||
this.router.navigate(['/unauthorized']);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Profile Page User Side
|
||||
*/
|
||||
|
||||
body {
|
||||
display: grid;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: auto auto auto auto auto auto;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
|
||||
background-color: lightgreen;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#Header {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/7;
|
||||
background-color: green;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* past contests table */
|
||||
#PastContests {
|
||||
grid-column: 1/4;
|
||||
grid-row: 2/6;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#Past_Contests {
|
||||
margin: 10px;
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* stuff with no ID */
|
||||
table {
|
||||
width: 100%;
|
||||
border: 3px solid black;
|
||||
border-collapse: seperate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
}
|
||||
table thead {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
z-index: 1;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
td {
|
||||
border: 3px solid black;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
}
|
||||
h1 {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Leaderboard popup window */
|
||||
#Leaderboard {
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h1 {
|
||||
grid-row: 1/2;
|
||||
grid-column: 2/5;
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h2 {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
#LeaderTable {
|
||||
grid-column: 2/6;
|
||||
grid-row: 4/5;
|
||||
margin: 10px;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Leaderboard</title>
|
||||
</head>
|
||||
<body id="Leaderboard">
|
||||
|
||||
<div id="Header">
|
||||
<h1> Leaderboard For </h1>
|
||||
<h2 id="ContestName"> </h2>
|
||||
</div>
|
||||
|
||||
<!-- table for leaderboard -->
|
||||
<div id="LeaderTable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Rank </th>
|
||||
<th> Name </th>
|
||||
<th> Flags </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="LeaderTBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LeaderboardComponent } from './leaderboard.component';
|
||||
|
||||
describe('LeaderboardComponent', () => {
|
||||
let component: LeaderboardComponent;
|
||||
let fixture: ComponentFixture<LeaderboardComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LeaderboardComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LeaderboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-leaderboard',
|
||||
imports: [],
|
||||
templateUrl: './leaderboard.component.html',
|
||||
styleUrl: './leaderboard.component.css'
|
||||
})
|
||||
export class LeaderboardComponent {
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
html, app-login html {
|
||||
background-color: green;
|
||||
height: 100vh;
|
||||
}
|
||||
app-login body {
|
||||
height: 92vh;
|
||||
margin: 25px;
|
||||
display: grid;
|
||||
background-color: white;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
|
||||
#inputFields {
|
||||
grid-row: 2/3;
|
||||
grid-column: 2/3;
|
||||
background-color: green;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
}
|
||||
#loginform {
|
||||
background-color: lightgreen;
|
||||
margin: 10px;
|
||||
height: 65%;
|
||||
}
|
||||
#loginform * {
|
||||
margin-top: 50px;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
h1 {
|
||||
font-size: 400%;
|
||||
font-weight: bold;
|
||||
}
|
||||
button {
|
||||
width: 12%;
|
||||
height: 7%;
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
background-color: lawngreen;
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
font-size: 100%;
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Login for CTF
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="inputFields">
|
||||
<h1>NMU CTF</h1>
|
||||
<form id="loginform" (ngSubmit)="SendCreds()">
|
||||
<br>
|
||||
<label for="EM">Email:</label>
|
||||
<input id="EM" type="text" [(ngModel)]="email" name="email" />
|
||||
<br>
|
||||
<label for="PS">Password:</label>
|
||||
<input *ngIf="!showPass" id="PS" type="password" [(ngModel)]="password" name="password" />
|
||||
<input *ngIf="showPass" id="PS" type="text" [(ngModel)]="password" name="password" />
|
||||
<br>
|
||||
<label for="ShowPass">Show Password</label>
|
||||
<input type="checkbox" id="ShowPass" (change)="showPassword()" />
|
||||
<br>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoginComponent } from './login.component';
|
||||
|
||||
describe('LoginComponent', () => {
|
||||
let component: LoginComponent;
|
||||
let fixture: ComponentFixture<LoginComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LoginComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LoginComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
import { Component, ViewEncapsulation } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgIf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
imports: [FormsModule, NgIf],
|
||||
templateUrl: './login.component.html',
|
||||
|
||||
styleUrls: ['./login.component.css'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
|
||||
export class LoginComponent {
|
||||
email: string = '';
|
||||
password: string = '';
|
||||
showPass: boolean = false;
|
||||
constructor(private router: Router) {}
|
||||
async SendCreds() {
|
||||
const data = { email: this.email, password: this.password };
|
||||
const res = await fetch('api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (res.ok) { // okay response from server
|
||||
const RB = await res.json();
|
||||
if (RB && RB.redirectTo) { // redirect client to new webpage
|
||||
sessionStorage.setItem('email', RB.email);
|
||||
sessionStorage.setItem('role', RB.redirectTo === '/admin-contest' ? 'admin' : 'user');
|
||||
this.router.navigate([RB.redirectTo]);
|
||||
}
|
||||
else { // error handling
|
||||
console.error("redirect URL issue");
|
||||
}
|
||||
}
|
||||
else { // what user entered doesnt match up to db
|
||||
alert("Invalid Credentials");
|
||||
}
|
||||
}
|
||||
|
||||
showPassword() {
|
||||
this.showPass = !this.showPass;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
export interface Admin {
|
||||
AdminID: number,
|
||||
Email: string,
|
||||
Password: string,
|
||||
ActiveFlag: string
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export interface Contest {
|
||||
ContestID: number,
|
||||
Name: string,
|
||||
IsActive: number,
|
||||
AdminID: number,
|
||||
Description: string
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
export interface Flag{
|
||||
FlagID: number,
|
||||
Name: string,
|
||||
Description: string,
|
||||
ContestID: number,
|
||||
Image: string,
|
||||
Path: string,
|
||||
Hint1: string,
|
||||
Hint2: string,
|
||||
Hint3: string
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
export interface Image{
|
||||
ImageID: number,
|
||||
Name: string,
|
||||
AdminID: number
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export interface Submission {
|
||||
SubmissionID: number,
|
||||
UserID: number,
|
||||
FlagID: number,
|
||||
IsCorrect: boolean,
|
||||
Attempts: number
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
export interface User{
|
||||
UserID: number,
|
||||
Name: string,
|
||||
Email: string,
|
||||
Password: string,
|
||||
Flags: number,
|
||||
ActiveFlag: string,
|
||||
AdminID: number
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Modifying Contests
|
||||
|
||||
*/
|
||||
body {
|
||||
background-color: lightblue;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
/* Header stuff */
|
||||
#HeaderStuff {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/3;
|
||||
background-color: blue;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
#DoneButt {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
background-color: lawngreen;
|
||||
}
|
||||
|
||||
/* Contests */
|
||||
#Contests {
|
||||
background-color: blue;
|
||||
grid-row: 2/3;
|
||||
grid-column: 1/2;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selected-contest {
|
||||
background-color: lawngreen;
|
||||
color: blue;
|
||||
border: 2px solid lawngreen;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#ContestContent, #FIContent {
|
||||
background-color: white;
|
||||
margin: 10px;
|
||||
text-align: center;
|
||||
height: 75%;
|
||||
}
|
||||
#ContestList, #FIList {
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#ActiveSpan {
|
||||
color: lawngreen;
|
||||
font-weight: bold;
|
||||
}
|
||||
#FISpan {
|
||||
color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Flags and Images */
|
||||
#FlagsAndImages {
|
||||
background-color: blue;
|
||||
grid-row: 2/3;
|
||||
grid-column: 2/3;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Terminal area */
|
||||
#Terminal {
|
||||
background-color: blue;
|
||||
grid-row: 3/4;
|
||||
grid-column: 1/3;
|
||||
margin: 50px;
|
||||
height: 525px;
|
||||
}
|
||||
#TerminalHeader {
|
||||
text-align: center;
|
||||
}
|
||||
#TerminalHeader select, input {
|
||||
margin: 10px;
|
||||
width: 150px;
|
||||
height: 25px;
|
||||
}
|
||||
#TerminalHeader input {
|
||||
width: 150px;
|
||||
height: 25px;
|
||||
}
|
||||
#Linux_Shell {
|
||||
background-color: black;
|
||||
height: 70%;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
#Buttons {
|
||||
background-color: blue;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
input {
|
||||
font-weight: bold;
|
||||
width: 20%;
|
||||
height: 10%;
|
||||
font-size: 15px;
|
||||
}
|
||||
.AddButt {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
.DeleteButt {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* selected items */
|
||||
.selected {
|
||||
background-color: lightgray;
|
||||
}
|
||||
.selected span {
|
||||
background-color: black;
|
||||
}
|
||||
.selected-flag {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
/* tooltips */
|
||||
.tooltip {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: inline;
|
||||
}
|
||||
.tooltip:hover .tooltipText {
|
||||
visibility: visible;
|
||||
}
|
||||
.tooltip .tooltipText {
|
||||
visibility: hidden;
|
||||
height: auto;
|
||||
background-color: gray;
|
||||
color: black;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Dialog box for deleting flags and images */
|
||||
#DialogBox {
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 100px;
|
||||
}
|
||||
#DialogBox input {
|
||||
width: 50%;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* anything else not related to any of above */
|
||||
h1 {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
display: inline;
|
||||
}
|
||||
ul {
|
||||
list-style-type: none;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
height: 50%;
|
||||
}
|
||||
select, option {
|
||||
background-color: white;
|
||||
}
|
||||
li {
|
||||
font-size: 20px;
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
<!--
|
||||
Jordan Latimer
|
||||
Alex Miller
|
||||
|
||||
Modify Contest screen for Admin
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<title>Contest Menu</title>
|
||||
</head>
|
||||
<body>
|
||||
<!-- header stuff -->
|
||||
<div id="HeaderStuff">
|
||||
<h1> Modify Contests </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can change anything involving <span style="color:white;">Contests</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<input type="button" id="DoneButt" (click)="navtoPageAP()" value="Back to Profile">
|
||||
</div>
|
||||
|
||||
<!-- Contests -->
|
||||
<div id="Contests">
|
||||
<h1> Contests </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
select a <span style="color:white;">Contest</span> by <i>clicking</i> on one
|
||||
<br>
|
||||
<span style="color:lawngreen;">Add:</span> add a new contest
|
||||
<br>
|
||||
<span style="color:blue;">Active:</span> set a <i>selected</i> contest active and available for your students
|
||||
<br>
|
||||
<span style="color:red;">Delete:</span> delete a <i>selected</i> contest
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<div id="ContestContent">
|
||||
|
||||
<!-- buttons to manipulate contests -->
|
||||
<div id="Buttons">
|
||||
<input class="AddButt" type="button" (click)="addContestPopup()" value="Add">
|
||||
<input type="button" (click)="setContestActive()" value="Activate">
|
||||
<input class="DeleteButt" type="button" (click)="deleteContest();" value="Delete">
|
||||
</div>
|
||||
|
||||
<!-- list of contests -->
|
||||
<div id="ContestList">
|
||||
<ul id="ContestListItems">
|
||||
<li *ngFor="let contest of contests" (click)="selectContest(contest.ContestID)"
|
||||
[ngClass]="{'selected-contest': contest.ContestID === selectedContestId}">
|
||||
{{ contest.Name }}
|
||||
<span *ngIf="contest.ContestID === activeContest">⭐</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Flags and Images -->
|
||||
<div id="FlagsAndImages">
|
||||
<h1> Flags and Images </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
when selecting a contest, the <span style="color:white;">Flags</span> and <span style="color:white;">Images</span> of that contest will show up here.
|
||||
<br>
|
||||
each <span style="color:white;">Flag</span> <span style="color:red;">NEEDS</span> a <i>selected</i> contest to be <span style="color:lawngreen;">Added</span>
|
||||
<br>
|
||||
this is not the case for <span style="color:white;">Images</span>
|
||||
<br>
|
||||
Some <span style="color:white;">Flags</span> may show up with <span style="color: red;">NONE</span>
|
||||
but be in different <span style="color:white;">Contests</span>
|
||||
you can select <span style="color:white;">Flags</span> to delete or change by <i>clicking</i> on them.
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<div id="FIContent">
|
||||
|
||||
<!-- buttons for flags and images -->
|
||||
<div id="Buttons">
|
||||
<input class="AddButt" type="button" (click)="addFlagPopup()" value="Add Flag">
|
||||
<input class="DeleteButt" type="button" (click)="showDialogBox()" value="Delete">
|
||||
<input class="AddButt" type="button" value="Add Image" (click)="navtoPageAI()">
|
||||
</div>
|
||||
|
||||
<!-- list of flags and images -->
|
||||
<div id="FIList">
|
||||
<ul id="FIListItems">
|
||||
<li *ngFor="let flag of flagsForContest" (click)="selectFlag(flag)"
|
||||
[ngClass]="{'selected-contest': flag.FlagID === selectedFlag?.FlagID}">
|
||||
{{ flag.Name }} ({{flag.Image}})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- linux shell to test images -->
|
||||
<div id="Terminal">
|
||||
<div id="TerminalHeader">
|
||||
<h1> Test Images </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can <i>test</i> the <span style="color:white;">Images</span> you create.
|
||||
<br>
|
||||
Simply select one from the dropdown and press <i>Test Image</i>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<select id="Images" [(ngModel)]="selectedImage">
|
||||
<option disabled selected hidden> Select an Image </option>
|
||||
<option *ngFor="let image of allImages" [value] ="image.Name">{{image.Name}}</option>
|
||||
</select>
|
||||
<input type="button" value="Test Image" (click)="TestImage()">
|
||||
|
||||
</div>
|
||||
<!-- the actual shell -->
|
||||
<div id="Linux_Shell">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dialog box for deleting flag or image -->
|
||||
<dialog id="DialogBox" [open]="isDialogVisible">
|
||||
<h3> Which would you want to Delete: </h3>
|
||||
<p id="FlagImage">{{ selectedFlag?.Image }}</p>
|
||||
<input type="button" value="Delete Flag" (click)="DeleteFlag()">
|
||||
<input type="button" value="Delete Image" (click)="DeleteImage()">
|
||||
<input type="button" value="Cancel" (click)="closeDialogBox()">
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModifyContestComponent } from './modify-contest.component';
|
||||
|
||||
describe('ModifyContestComponent', () => {
|
||||
let component: ModifyContestComponent;
|
||||
let fixture: ComponentFixture<ModifyContestComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ModifyContestComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ModifyContestComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,210 @@
|
||||
import { Component, Renderer2 } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TerminalService } from '../Helper/terminal.service';
|
||||
import { getEmail, gotoPage, Popup, updateContainer } from '../Helper/Helpers';
|
||||
import { ModifyContestService } from './modify-contest.service';
|
||||
import { Contest } from '../models/contest.model';
|
||||
import { NgFor, CommonModule } from '@angular/common';
|
||||
import { Flag } from '../models/flag.model';
|
||||
import { Image } from '../models/image.model';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modify-contest',
|
||||
imports: [NgFor, CommonModule, FormsModule],
|
||||
templateUrl: './modify-contest.component.html',
|
||||
styleUrl: './modify-contest.component.css'
|
||||
})
|
||||
|
||||
export class ModifyContestComponent {
|
||||
contests: Contest[] = [];
|
||||
activeContest: number | null = null;
|
||||
flagsForContest: Flag[] = [];
|
||||
selectedContestId: number = 0;
|
||||
selectedFlag: Flag | null = null;
|
||||
allImages: Image[] = []
|
||||
isDialogVisible = false;
|
||||
selectedImage: string = "Select an Image";
|
||||
constructor(private renderer: Renderer2, private router: Router, private modifyContestService: ModifyContestService, private terminalService: TerminalService){}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.terminalService.reattachTerminal();
|
||||
|
||||
// style class for blue border
|
||||
this.renderer.addClass(document.documentElement, 'modify');
|
||||
|
||||
this.loadContests();
|
||||
this.getImages();
|
||||
|
||||
window.addEventListener("message", this.handlePopupMessage.bind(this));
|
||||
}
|
||||
|
||||
// set app back to default when leaving this page
|
||||
ngOnDestroy(): void {
|
||||
this.renderer.removeClass(document.documentElement, 'modify');
|
||||
}
|
||||
|
||||
handlePopupMessage(event: MessageEvent): void {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
if (event.data?.type === "FLAG_ADDED" && event.data?.contestId) {
|
||||
this.modifyContestService.loadFlagsForContest(event.data.contestId)
|
||||
.then(flags => this.flagsForContest = flags)
|
||||
.catch(err => console.error('Error reloading flags:', err));
|
||||
}
|
||||
}
|
||||
|
||||
// Method to load contests
|
||||
async loadContests(): Promise<void> {
|
||||
const email = this.getEmail();
|
||||
try {
|
||||
const contestsData = await this.modifyContestService.loadContests(email);
|
||||
this.contests = contestsData || []; // Populate contests array
|
||||
this.activeContest = await this.modifyContestService.getActiveContest(this.getEmail()); //Getting the active contest
|
||||
} catch (error) {
|
||||
console.error('Error loading contests:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveContest(): Promise<void> {
|
||||
this.activeContest = await this.modifyContestService.getActiveContest(this.getEmail());
|
||||
}
|
||||
|
||||
async getImages(): Promise<void>{
|
||||
const email = this.getEmail();
|
||||
try{
|
||||
const imageData = await this.modifyContestService.getImages(email);
|
||||
this.allImages = imageData || [];
|
||||
}catch(error){
|
||||
console.error('Error loading images');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to add a contest
|
||||
addContestPopup(): void {
|
||||
Popup('/add-contest');
|
||||
}
|
||||
|
||||
addFlagPopup(): void{
|
||||
if(this.selectedContestId === 0)
|
||||
alert("Select a contest to add a flag!");
|
||||
else{
|
||||
Popup('/add-flag');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
navtoPageAI(): void{
|
||||
gotoPage(this.router, '/create-image');
|
||||
}
|
||||
|
||||
// Method to set a contest as active
|
||||
async setContestActive(): Promise<void> {
|
||||
if (this.selectedContestId !== null) {
|
||||
try {
|
||||
const email = this.getEmail();
|
||||
await this.modifyContestService.setContestActive(this.selectedContestId, email);
|
||||
this.loadContests();
|
||||
console.log('Contest set as active');
|
||||
} catch (error) {
|
||||
console.error('Error activating contest:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteContest(): Promise<void> {
|
||||
if (this.selectedContestId === 0)
|
||||
alert("Select a contest to delete!");
|
||||
else if(this.activeContest === this.selectedContestId)
|
||||
alert("You can't delete an active contest!");
|
||||
else{
|
||||
try {
|
||||
await this.modifyContestService.deleteContest(this.selectedContestId);
|
||||
console.log('Contest deleted');
|
||||
this.loadContests(); // Refresh the contest list after deletion
|
||||
this.selectedContestId = 0;
|
||||
this.flagsForContest = [];
|
||||
} catch (error) {
|
||||
console.error('Error deleting contest:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectContest(contestId: number): void {
|
||||
this.selectedContestId = contestId;
|
||||
this.modifyContestService.loadFlagsForContest(contestId)
|
||||
.then((flags) => {
|
||||
this.flagsForContest = flags;
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load flags:', err);
|
||||
})
|
||||
sessionStorage.setItem('selectedContestID', this.selectedContestId.toString());
|
||||
console.log('Selected contest ID:', contestId);
|
||||
}
|
||||
|
||||
selectFlag(flag: Flag): void{
|
||||
this.selectedFlag = flag
|
||||
}
|
||||
|
||||
getEmail(): string {
|
||||
return getEmail();
|
||||
}
|
||||
|
||||
navtoPageAP() {
|
||||
gotoPage(this.router, '/admin-profile');
|
||||
}
|
||||
|
||||
showDialogBox(): void{
|
||||
if(!this.selectedFlag || this.selectedFlag.Name === null){
|
||||
alert('You must select a flag first');
|
||||
return;
|
||||
}
|
||||
console.log('Opening dialog for flag:', this.selectedFlag);
|
||||
this.isDialogVisible = true;
|
||||
}
|
||||
|
||||
closeDialogBox(): void{
|
||||
this.isDialogVisible = false;
|
||||
}
|
||||
|
||||
async DeleteFlag(): Promise<void>{
|
||||
if(!this.selectedFlag){
|
||||
console.log("No flag selected");
|
||||
alert("Please select a flag to delete.");
|
||||
return;
|
||||
}
|
||||
try{
|
||||
await this.modifyContestService.DeleteFlag(this.selectedFlag.FlagID);
|
||||
this.flagsForContest = await this.modifyContestService.loadFlagsForContest(this.selectedContestId);
|
||||
} catch(error){
|
||||
console.error("Failed deleting flag:", error);
|
||||
}
|
||||
console.log("Deleted Flag");
|
||||
this.isDialogVisible = false;
|
||||
this.selectedFlag = null;
|
||||
}
|
||||
|
||||
DeleteImage(): void{
|
||||
this.closeDialogBox();
|
||||
}
|
||||
|
||||
async TestImage(): Promise<void>{
|
||||
if(this.selectedImage === "Select an Image"){
|
||||
alert('you must select an Image first');
|
||||
return;
|
||||
}
|
||||
try{
|
||||
await this.modifyContestService.setNewActiveFlag(this.selectedImage, getEmail())
|
||||
} catch (error) {
|
||||
console.error("Failed setting up new image", error);
|
||||
}
|
||||
await updateContainer(getEmail());
|
||||
this.ClearAndSetTerminal();
|
||||
}
|
||||
|
||||
// clear and set terminal for new flag
|
||||
async ClearAndSetTerminal() {
|
||||
await this.terminalService.ClearTerminal();
|
||||
await this.terminalService.reattachTerminal();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModifyContestService } from './modify-contest.service';
|
||||
|
||||
describe('ModifyContestService', () => {
|
||||
let service: ModifyContestService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ModifyContestService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,232 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ModifyContestService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
async getImages(email: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/getImages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch images');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching images:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadContests(email: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/contests/getContests', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch contests');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching contests:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async loadFlagsForContest(contestId: number): Promise<any> {
|
||||
if(contestId === 0)
|
||||
return [];
|
||||
try {
|
||||
const response = await fetch('api/flags/getAllFlagsFromContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ contest: contestId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch flags');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching flags:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async addContest(data: any): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/AddContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add contest');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error adding contest:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteContest(contestId: number): Promise<any> {
|
||||
// Send a POST request to delete the contest by its name
|
||||
let data = {contest: contestId};
|
||||
await this.deleteFlagsFromContest(contestId);
|
||||
let res = await fetch('api/DeleteContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
console.log("Success: deleting contest");
|
||||
} else {
|
||||
console.log("ERROR: deleting contest");
|
||||
alert("Failed to delete contest.");
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFlagsFromContest(contestId: number): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/DeleteFlagsFromContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ contest: contestId })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete flags');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error deleting flags:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async AddFlag(data: any): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/AddFlag', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add flag');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error adding flag:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async DeleteFlag(flagId: number): Promise<any> {
|
||||
let data = { flag: flagId };
|
||||
let res = await fetch('api/DeleteFlag', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async setNewActiveFlag(flagImage: string, Email: string):Promise<any> {
|
||||
const image = flagImage;
|
||||
// set up the container for the image
|
||||
const data = { FlagImage: image, email: Email };
|
||||
const res = await fetch('/api/setNewActiveFlag', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
async setContestActive(contestId: number, email: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/setContestActive', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ contest: contestId, email })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to set contest active');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error setting contest active:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteImage(imageName: string): Promise<any> {
|
||||
try {
|
||||
const response = await fetch('api/DeleteImageReplaceFlags', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ images: imageName })
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete image');
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error deleting image:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveContest(Email: string): Promise<any>{
|
||||
const data = { email: Email };
|
||||
const res = await fetch('api/contests/getActiveContest', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type' : 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (res.ok) {
|
||||
let contest = await res.json();
|
||||
return contest.ContestID;
|
||||
}
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SocketIOService } from './socket-io.service';
|
||||
|
||||
describe('SocketIOService', () => {
|
||||
let service: SocketIOService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(SocketIOService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,48 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SocketIOService {
|
||||
private socket: Socket;
|
||||
|
||||
constructor() {
|
||||
this.socket = io("/api/socket.io", {
|
||||
path: '/api/socket.io',
|
||||
transports: ['websocket'], // or ['polling', 'websocket'] if you want fallback
|
||||
autoConnect: false,
|
||||
});
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (!this.socket.connected) {
|
||||
console.log("Attempting to connect...");
|
||||
this.socket.connect();
|
||||
|
||||
this.socket.on("connect", () => console.log("Connected to Socket.IO!"));
|
||||
this.socket.on("connect_error", (err) => console.error("Socket.IO Connection Error:", err));
|
||||
}
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
if (this.socket.connected) {
|
||||
this.socket.disconnect();
|
||||
console.log("Disconnected from Socket.IO.");
|
||||
}
|
||||
}
|
||||
|
||||
listen<T = any>(eventName: string): Observable<T> {
|
||||
return new Observable<T>((subscriber) => {
|
||||
const handler = (data: T) => subscriber.next(data);
|
||||
this.socket.on(eventName, handler);
|
||||
|
||||
return () => this.socket.off(eventName, handler); // cleanup
|
||||
});
|
||||
}
|
||||
|
||||
emit(eventName: string, data: any): void {
|
||||
this.socket.emit(eventName, data);
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<h1>Thought you could pull a fast one eh?</h1>
|
||||
<p>I dont think so</p>
|
||||
<h2>👎</h2>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UNAUTHORIZEDComponent } from './unauthorized.component';
|
||||
|
||||
describe('UNAUTHORIZEDComponent', () => {
|
||||
let component: UNAUTHORIZEDComponent;
|
||||
let fixture: ComponentFixture<UNAUTHORIZEDComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UNAUTHORIZEDComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UNAUTHORIZEDComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-unauthorized',
|
||||
imports: [],
|
||||
templateUrl: './unauthorized.component.html',
|
||||
styleUrl: './unauthorized.component.css'
|
||||
})
|
||||
export class UNAUTHORIZEDComponent {
|
||||
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
.body {
|
||||
height: 98vh;
|
||||
display: grid;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: auto auto auto auto auto auto;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
|
||||
background-color: lightgreen;
|
||||
}
|
||||
html {
|
||||
background-color: green;
|
||||
}
|
||||
.bg {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#Header {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/7;
|
||||
background-color: green;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
#Header input {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#ContestsButt {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#LogoutButt {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* past contests table */
|
||||
#PastContests {
|
||||
grid-column: 1/4;
|
||||
grid-row: 2/6;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#Past_Contests {
|
||||
margin: 10px;
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
/* record table */
|
||||
#Record, #CurrentContest {
|
||||
grid-column: 5/7;
|
||||
grid-row: 2/3;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#RecordTable, #Current_Contest {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* Usernames */
|
||||
#Username {
|
||||
grid-row: 3/5;
|
||||
grid-column: 5/7;
|
||||
background-color: green;
|
||||
text-align: center;
|
||||
margin: 50px;
|
||||
}
|
||||
#generate {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
#setname {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#generate, #setname {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* tooltips */
|
||||
.tooltip {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: inline;
|
||||
}
|
||||
.tooltip:hover .tooltipText {
|
||||
visibility: visible;
|
||||
}
|
||||
.tooltip .tooltipText {
|
||||
visibility: hidden;
|
||||
height: auto;
|
||||
background-color: gray;
|
||||
color: black;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* stuff with no ID */
|
||||
table {
|
||||
width: 100%;
|
||||
border: 3px solid black;
|
||||
border-collapse: seperate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
}
|
||||
table thead {
|
||||
top: 0;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
z-index: 1;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
td {
|
||||
border: 3px solid black;
|
||||
text-align: center;
|
||||
height: 50px;
|
||||
}
|
||||
button {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background-color: lawngreen;
|
||||
font-weight: bold;
|
||||
}
|
||||
h1 {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Leaderboard popup window */
|
||||
#Leaderboard {
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h1 {
|
||||
grid-row: 1/2;
|
||||
grid-column: 2/5;
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h2 {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
#LeaderTable {
|
||||
grid-column: 2/6;
|
||||
grid-row: 4/5;
|
||||
margin: 10px;
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<div class="bg">
|
||||
<div class="body">
|
||||
<!-- header -->
|
||||
<div id="Header">
|
||||
<h1> Menu </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is the main place for you, the <span style="color:white">User</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<input id="ContestsButt" type="button" value="Profile" (click)="navtoPage()">
|
||||
</div>
|
||||
|
||||
<!-- past contests -->
|
||||
<div id="PastContests">
|
||||
<h1> Past Contests </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is every past contest that's still around
|
||||
<br>
|
||||
you can view the leaderboard or hop in to take a look
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- past contest table -->
|
||||
<div id="Past_Contests">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Contest Name </th>
|
||||
<th> Flags </th>
|
||||
<th> Leaderboard </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="UPCTbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- current contest-->
|
||||
<div id="CurrentContest">
|
||||
<h1> Current Contest </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where the current contest is shown
|
||||
<br>
|
||||
click <span style="color:lawngreen">Join</span> to join in on the fun
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- Current Contest -->
|
||||
<div id="Current_Contest">
|
||||
<table>
|
||||
<thead>
|
||||
<th> Current Contest </th>
|
||||
<th> Flags </th>
|
||||
<th> Join </th>
|
||||
</thead>
|
||||
<tbody id="UCCTbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserMenuComponent } from './user-menu.component';
|
||||
|
||||
describe('UserMenuComponent', () => {
|
||||
let component: UserMenuComponent;
|
||||
let fixture: ComponentFixture<UserMenuComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserMenuComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserMenuComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||
import { Router } from '@angular/router'
|
||||
import { Contest } from '../models/contest.model.js';
|
||||
import { PopulateTable, InsertIntoTable, gotoPage, getContests } from '../Helper/Helpers.js';
|
||||
@Component({
|
||||
selector: 'app-user-menu',
|
||||
imports: [],
|
||||
templateUrl: './user-menu.component.html',
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
styleUrl: './user-menu.component.css'
|
||||
})
|
||||
|
||||
export class UserMenuComponent implements OnInit{
|
||||
constructor(private router: Router){}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
let data: Contest[] = await getContests();
|
||||
InsertIntoTable(data, this.router);
|
||||
}
|
||||
|
||||
navtoPage(): void {
|
||||
gotoPage(this.router, '/user-profile');
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Profile Page User Side
|
||||
*/
|
||||
|
||||
body {
|
||||
height: 98vh;
|
||||
display: grid;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: auto auto auto auto auto auto;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
|
||||
background-color: lightgreen;
|
||||
}
|
||||
html {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#Header {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/7;
|
||||
background-color: green;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
#Header input {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#ContestsButt {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#LogoutButt {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* past contests table */
|
||||
#PastContests {
|
||||
grid-column: 1/4;
|
||||
grid-row: 2/6;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#Past_Contests {
|
||||
margin: 10px;
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
/* record table */
|
||||
#Record, #CurrentContest {
|
||||
grid-column: 5/7;
|
||||
grid-row: 2/3;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#RecordTable, #Current_Contest {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* Usernames */
|
||||
#Username {
|
||||
grid-row: 3/5;
|
||||
grid-column: 5/7;
|
||||
background-color: green;
|
||||
text-align: center;
|
||||
margin: 50px;
|
||||
}
|
||||
#generate {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
#setname {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#generate, #setname {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* tooltips */
|
||||
.tooltip {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: inline;
|
||||
}
|
||||
.tooltip:hover .tooltipText {
|
||||
visibility: visible;
|
||||
}
|
||||
.tooltip .tooltipText {
|
||||
visibility: hidden;
|
||||
height: auto;
|
||||
background-color: gray;
|
||||
color: black;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* stuff with no ID */
|
||||
h1 {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Leaderboard popup window */
|
||||
#Leaderboard {
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h1 {
|
||||
grid-row: 1/2;
|
||||
grid-column: 2/5;
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h2 {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
#LeaderTable {
|
||||
grid-column: 2/6;
|
||||
grid-row: 4/5;
|
||||
margin: 10px;
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Profile Page for Users
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NMU CTF Profile Page</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<div id="Header">
|
||||
<h1> Profile </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is everything about your profile
|
||||
<br>
|
||||
you can view your Past Contests, Record,
|
||||
<br>
|
||||
and change your username
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<input id="ContestsButt" type="button" value="Back to Contests" (click)="navtoPage()">
|
||||
<input id="LogoutButt" type="button" value="Logout" (click)="onLogout()">
|
||||
</div>
|
||||
<!-- Past contest table -->
|
||||
<div id="PastContests">
|
||||
<h1> Past Contests </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is every past contest that's still around
|
||||
<br>
|
||||
you can see the <span style="color:white">Leaderboard</span> by clicking
|
||||
<br>
|
||||
on <span style="color:lawngreen">View</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- past contest table -->
|
||||
<div id="Past_Contests">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Contest Name </th>
|
||||
<th> Flags </th>
|
||||
<th> Leaderboard </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="UPCTBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- chaning username -->
|
||||
<div id="Username">
|
||||
<h1> Edit Username</h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can change your username
|
||||
<br>
|
||||
simply click <span style="color:blue">Generate</span> and a random
|
||||
<br>
|
||||
username will be generated, keep going until you find one you like and
|
||||
<br>
|
||||
click <span style="color:lawngreen">Set Name</span> to set that as your username
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- Name generator and logout button -->
|
||||
<div id="Name">
|
||||
<input type="text" id="NameInput" [(ngModel)]="NameInput" readonly>
|
||||
<input id="generate" type="button" value="Generate" (click)="onGetNewName()">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<input id="setname" type="button" value="Set Name" (click)="onSetNewName()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- users record -->
|
||||
<div id="Record">
|
||||
<h1> Record </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can view your <span style="color:white">Record</span>
|
||||
<br>
|
||||
to see how well you are doing with all <span style="color:white">Contests</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- record table -->
|
||||
<div id="RecordTable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Completed </th>
|
||||
<th> Total </th>
|
||||
<th> Record </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="URTbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserProfileACComponent } from './user-profile-ac.component';
|
||||
|
||||
describe('UserProfileACComponent', () => {
|
||||
let component: UserProfileACComponent;
|
||||
let fixture: ComponentFixture<UserProfileACComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserProfileACComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserProfileACComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,37 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { loadTable, getNewName, setNewName } from '../Helper/User-pfp';
|
||||
import { Router } from '@angular/router';
|
||||
import { gotoPage, logOut } from '../Helper/Helpers';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-profile-ac',
|
||||
imports: [FormsModule],
|
||||
templateUrl: './user-profile-ac.component.html',
|
||||
styleUrl: './user-profile-ac.component.css'
|
||||
})
|
||||
export class UserProfileACComponent {
|
||||
|
||||
constructor(private router: Router){}
|
||||
|
||||
NameInput: string = '';
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.NameInput = await loadTable();
|
||||
}
|
||||
|
||||
navtoPage(): void {
|
||||
gotoPage(this.router, '/contest-page');
|
||||
}
|
||||
|
||||
onGetNewName(): void{
|
||||
this.NameInput = getNewName();
|
||||
}
|
||||
|
||||
onSetNewName(): void{
|
||||
setNewName(this.NameInput);
|
||||
}
|
||||
|
||||
onLogout(): void{
|
||||
logOut(this.router);
|
||||
}
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
CSS for Profile Page User Side
|
||||
*/
|
||||
|
||||
body {
|
||||
height: 98vh;
|
||||
display: grid;
|
||||
|
||||
/* columns and rows sizes */
|
||||
grid-template-columns: auto auto auto auto auto auto;
|
||||
grid-template-rows: auto auto auto auto auto;
|
||||
|
||||
background-color: lightgreen;
|
||||
}
|
||||
html {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#Header {
|
||||
grid-row: 1/2;
|
||||
grid-column: 1/7;
|
||||
background-color: green;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
#Header input {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#ContestsButt {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#LogoutButt {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* past contests table */
|
||||
#PastContests {
|
||||
grid-column: 1/4;
|
||||
grid-row: 2/6;
|
||||
margin: 50px;
|
||||
height: 500px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#Past_Contests {
|
||||
margin: 10px;
|
||||
height: 75%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
|
||||
/* record table */
|
||||
#Record, #CurrentContest {
|
||||
grid-column: 5/7;
|
||||
grid-row: 2/3;
|
||||
margin: 50px;
|
||||
text-align: center;
|
||||
background-color: green;
|
||||
}
|
||||
#RecordTable, #Current_Contest {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
/* Usernames */
|
||||
#Username {
|
||||
grid-row: 3/5;
|
||||
grid-column: 5/7;
|
||||
background-color: green;
|
||||
text-align: center;
|
||||
margin: 50px;
|
||||
}
|
||||
#generate {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
#setname {
|
||||
background-color: lawngreen;
|
||||
}
|
||||
#generate, #setname {
|
||||
height: 25px;
|
||||
width: 150px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* tooltips */
|
||||
.tooltip {
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
display: inline;
|
||||
}
|
||||
.tooltip:hover .tooltipText {
|
||||
visibility: visible;
|
||||
}
|
||||
.tooltip .tooltipText {
|
||||
visibility: hidden;
|
||||
height: auto;
|
||||
background-color: gray;
|
||||
color: black;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* stuff with no ID */
|
||||
h1 {
|
||||
display: inline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Leaderboard popup window */
|
||||
#Leaderboard {
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h1 {
|
||||
grid-row: 1/2;
|
||||
grid-column: 2/5;
|
||||
text-align: center;
|
||||
}
|
||||
#Leaderboard h2 {
|
||||
grid-row: 2/3;
|
||||
grid-column: 3/4;
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
#LeaderTable {
|
||||
grid-column: 2/6;
|
||||
grid-row: 4/5;
|
||||
margin: 10px;
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
<!--
|
||||
Alex Miller
|
||||
Jordan Latimer
|
||||
|
||||
Profile Page before entering a contest for User
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NMU CTF User Profile Page</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Header -->
|
||||
<div id="Header">
|
||||
<h1> Profile </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is everything about your profile
|
||||
<br>
|
||||
you can view your Past Contests, Record,
|
||||
<br>
|
||||
and change your username
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
<input id="ContestsButt" type="button" value="Back to Menu" (click)="navtoPage()">
|
||||
<input id="LogoutButt" type="button" value="Logout" (click)="onLogout()">
|
||||
</div>
|
||||
|
||||
<!-- past contests -->
|
||||
<div id="PastContests">
|
||||
<h1> Past Contests </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is every past contest that's still around
|
||||
<br>
|
||||
you can see the <span style="color:white">Leaderboard</span> by clicking
|
||||
<br>
|
||||
on <span style="color:lawngreen">View</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- past contest table -->
|
||||
<div id="Past_Contests">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Contest Name </th>
|
||||
<th> Flags </th>
|
||||
<th> Leaderboard </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="UPCTBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- usernames -->
|
||||
<div id="Username">
|
||||
<h1> Edit Username </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can change your username
|
||||
<br>
|
||||
simply click <span style="color:blue">Generate</span> and a random
|
||||
<br>
|
||||
username will be generated, keep going until you find one you like and
|
||||
<br>
|
||||
click <span style="color:lawngreen">Set Name</span> to set that as your username
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- Name generator and logout button -->
|
||||
<div id="Name">
|
||||
<input type="text" id="NameInput" [(ngModel)]="NameInput" readonly>
|
||||
<input id="generate" type="button" value="Generate" (click)="onGetNewName()">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<input id="setname" type="button" value="Set Name" (click)="onSetNewName()">
|
||||
</div>
|
||||
</div>
|
||||
<!-- record -->
|
||||
<div id="Record">
|
||||
<h1> Record </h1>
|
||||
<div class="tooltip">
|
||||
?
|
||||
<span class="tooltipText">
|
||||
This is where you can view your <span style="color:white">Record</span>
|
||||
<br>
|
||||
to see how well you are doing with all <span style="color:white">Contests</span>
|
||||
</span>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<!-- record table -->
|
||||
<div id="RecordTable">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th> Completed </th>
|
||||
<th> Total </th>
|
||||
<th> Record </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="URTbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserProfileComponent } from './user-profile.component';
|
||||
|
||||
describe('UserProfileComponent', () => {
|
||||
let component: UserProfileComponent;
|
||||
let fixture: ComponentFixture<UserProfileComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserProfileComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UserProfileComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,38 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { loadTable } from '../Helper/User-pfp';
|
||||
import { gotoPage, logOut } from '../Helper/Helpers';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { getNewName, setNewName } from '../Helper/User-pfp';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-profile',
|
||||
imports: [FormsModule],
|
||||
templateUrl: './user-profile.component.html',
|
||||
styleUrl: './user-profile.component.css'
|
||||
})
|
||||
export class UserProfileComponent implements OnInit {
|
||||
|
||||
constructor(private router: Router){}
|
||||
|
||||
NameInput: string = '';
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.NameInput = await loadTable();
|
||||
}
|
||||
|
||||
navtoPage(): void {
|
||||
gotoPage(this.router, '/user-menu');
|
||||
}
|
||||
|
||||
onGetNewName(): void{
|
||||
this.NameInput = getNewName();
|
||||
}
|
||||
|
||||
onSetNewName(): void{
|
||||
setNewName(this.NameInput);
|
||||
}
|
||||
|
||||
onLogout(): void{
|
||||
logOut(this.router);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="default">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CTFFrontend</title>
|
||||
<base href="/" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self' ws://localhost:3000;"
|
||||
/>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue