diff options
author | sowgro <tpoke.ferrari@gmail.com> | 2025-03-30 16:26:12 -0400 |
---|---|---|
committer | sowgro <tpoke.ferrari@gmail.com> | 2025-03-30 16:26:12 -0400 |
commit | 27b2b418cc330e37f5802a81d678dd27259ee1e0 (patch) | |
tree | 893231478cb132731546ef7eb6b5c94d0c748091 | |
parent | e1eb3f16e10042c2539b56d6c2d2e33f07abf7d9 (diff) | |
parent | c6bbb29f42eaea7d0c8aebdb7b95be0287cbf4f9 (diff) | |
download | JellySolutions-27b2b418cc330e37f5802a81d678dd27259ee1e0.tar.gz JellySolutions-27b2b418cc330e37f5802a81d678dd27259ee1e0.tar.bz2 JellySolutions-27b2b418cc330e37f5802a81d678dd27259ee1e0.zip |
Merge branch 'main' into css
# Conflicts:
# ufund-ui/src/app/app-routing.module.ts
# ufund-ui/src/app/app.module.ts
# ufund-ui/src/app/components/login/login.component.html
Diffstat (limited to '')
8 files changed, 379 insertions, 7 deletions
diff --git a/ufund-api/src/main/java/com/ufund/api/ufundapi/controller/CupboardController.java b/ufund-api/src/main/java/com/ufund/api/ufundapi/controller/CupboardController.java index 55ee457..bbfd3f6 100644 --- a/ufund-api/src/main/java/com/ufund/api/ufundapi/controller/CupboardController.java +++ b/ufund-api/src/main/java/com/ufund/api/ufundapi/controller/CupboardController.java @@ -182,7 +182,7 @@ public class CupboardController { /** * Checks out a need by checkoutAmount * - * @param data JSON object with paramters needID and amount + * @param data JSON object with parameters needID and amount * @param key Key used to authenticate user * @return OK if successful, other statuses if failure */ diff --git a/ufund-api/src/test/java/com/ufund/api/ufundapi/controller/CupboardControllerTest.java b/ufund-api/src/test/java/com/ufund/api/ufundapi/controller/CupboardControllerTest.java index 89697bf..d775d14 100644 --- a/ufund-api/src/test/java/com/ufund/api/ufundapi/controller/CupboardControllerTest.java +++ b/ufund-api/src/test/java/com/ufund/api/ufundapi/controller/CupboardControllerTest.java @@ -23,10 +23,11 @@ public class CupboardControllerTest { private CupboardController cupboardController; private CupboardService mockCupboardService; private final String key = "dummyKey"; + private AuthService mockAuthService; @BeforeEach public void setupCupboardDAO() { - AuthService mockAuthService = mock(AuthService.class); + mockAuthService = mock(AuthService.class); mockCupboardService = mock(CupboardService.class); cupboardController = new CupboardController(mockCupboardService, mockAuthService); @@ -63,7 +64,8 @@ public class CupboardControllerTest { Map<String, Object> needMap = Map.ofEntries( entry("name", "Name"), entry("maxGoal", -100.0), - entry("type", "MONETARY")); + entry("type", "MONETARY") + ); var res = cupboardController.createNeed(needMap, key); @@ -77,7 +79,8 @@ public class CupboardControllerTest { Map<String, Object> needMap = Map.ofEntries( entry("name", "Name"), entry("maxGoal", 100.0), - entry("type", "MONETARY")); + entry("type", "MONETARY") + ); var res = cupboardController.createNeed(needMap, key); @@ -85,6 +88,36 @@ public class CupboardControllerTest { } @Test + public void createNeedConflict() throws IOException, DuplicateKeyException { + when(mockCupboardService.createNeed("Name", 100, Need.GoalType.MONETARY)).thenThrow(new DuplicateKeyException("")); + + Map<String, Object> needMap = Map.ofEntries( + entry("name", "Name"), + entry("maxGoal", 100.0), + entry("type", "MONETARY") + ); + + var res = cupboardController.createNeed(needMap, key); + + assertEquals(HttpStatus.CONFLICT, res.getStatusCode()); + } + + @Test + public void createNeedUnauthorized() throws IOException, IllegalAccessException { + doThrow(new IllegalAccessException()).when(mockAuthService).keyHasAccessToCupboard(key); + + Map<String, Object> needMap = Map.ofEntries( + entry("name", "Name"), + entry("maxGoal", 100.0), + entry("type", "MONETARY") + ); + + var res = cupboardController.createNeed(needMap, key); + + assertEquals(HttpStatus.UNAUTHORIZED, res.getStatusCode()); + } + + @Test public void getNeeds() throws IOException { var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); when(mockCupboardService.getNeeds()).thenReturn(new Need[]{need}); @@ -198,6 +231,36 @@ public class CupboardControllerTest { } @Test + public void updateNeedMissing() throws IOException { + var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); + when(mockCupboardService.updateNeed(need, 1)).thenReturn(null); + + var res = cupboardController.updateNeed(need, 1, key); + + assertEquals(HttpStatus.NOT_FOUND, res.getStatusCode()); + } + + @Test + public void updateNeedBadRequest() throws IOException { + var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); + when(mockCupboardService.updateNeed(need, 1)).thenThrow(new IllegalArgumentException()); + + var res = cupboardController.updateNeed(need, 1, key); + + assertEquals(HttpStatus.BAD_REQUEST, res.getStatusCode()); + } + + @Test + public void updateNeedUnauthorized() throws IOException, IllegalAccessException { + var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); + doThrow(new IllegalAccessException()).when(mockAuthService).keyHasAccessToCupboard(key); + + var res = cupboardController.updateNeed(need, 1, key); + + assertEquals(HttpStatus.UNAUTHORIZED, res.getStatusCode()); + } + + @Test public void deleteNeed() throws IOException { var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); when(mockCupboardService.getNeed(1)).thenReturn(need); @@ -219,6 +282,17 @@ public class CupboardControllerTest { } @Test + public void deleteNeedUnauthorized() throws IOException, IllegalAccessException { + var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); + when(mockCupboardService.getNeed(1)).thenReturn(need); + doThrow(new IllegalAccessException()).when(mockAuthService).keyHasAccessToCupboard(key); + + var res = cupboardController.deleteNeed(1, key); + + assertEquals(HttpStatus.UNAUTHORIZED, res.getStatusCode()); + } + + @Test public void deleteNeedIOException() throws IOException { var need = new Need("Name", 1, 100, Need.GoalType.MONETARY); when(mockCupboardService.getNeed(1)).thenReturn(need); @@ -228,4 +302,61 @@ public class CupboardControllerTest { assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, res.getStatusCode()); } + + @Test + public void checkoutNeeds() throws IOException, IllegalAccessException { + doNothing().when(mockCupboardService).checkoutNeed(0, 20, key); + + + Map<String, Integer> needMap = Map.ofEntries( + entry("needID", 0), + entry("amount", 20) + ); + + var res = cupboardController.checkoutNeeds(needMap, key); + + assertEquals(HttpStatus.OK, res.getStatusCode()); + } + + @Test + public void checkoutNeedsBadRequest() throws IOException, IllegalAccessException { + doThrow(new IllegalArgumentException()).when(mockCupboardService).checkoutNeed(0, 20, key); + + Map<String, Integer> needMap = Map.ofEntries( + entry("needID", 0), + entry("amount", 20) + ); + + var res = cupboardController.checkoutNeeds(needMap, key); + + assertEquals(HttpStatus.BAD_REQUEST, res.getStatusCode()); + } + + @Test + public void checkoutNeedsUnauthorized() throws IOException, IllegalAccessException { + doThrow(new IllegalAccessException()).when(mockCupboardService).checkoutNeed(0, 20, key); + + Map<String, Integer> needMap = Map.ofEntries( + entry("needID", 0), + entry("amount", 20) + ); + + var res = cupboardController.checkoutNeeds(needMap, key); + + assertEquals(HttpStatus.UNAUTHORIZED, res.getStatusCode()); + } + + @Test + public void checkoutNeedsInternalError() throws IOException, IllegalAccessException { + doThrow(new IOException()).when(mockCupboardService).checkoutNeed(0, 20, key); + + Map<String, Integer> needMap = Map.ofEntries( + entry("needID", 0), + entry("amount", 20) + ); + + var res = cupboardController.checkoutNeeds(needMap, key); + + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, res.getStatusCode()); + } } diff --git a/ufund-ui/src/app/app-routing.module.ts b/ufund-ui/src/app/app-routing.module.ts index c83db8a..89b6f67 100644 --- a/ufund-ui/src/app/app-routing.module.ts +++ b/ufund-ui/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ import {LoginComponent} from './components/login/login.component'; import {HomePageComponent} from './components/home-page/home-page.component'; import {FundingBasketComponent} from './components/funding-basket/funding-basket.component'; import {NeedPageComponent} from './components/need-page/need-page.component'; +import {SignupComponent} from './components/signup/signup.component'; const routes: Routes = [ { path: '', component: HomePageComponent, title: "Home | JS" }, @@ -13,7 +14,8 @@ const routes: Routes = [ { path: 'cupboard', component: CupboardComponent, title: "Cupboard | JS" }, { path: 'dashboard', component: DashboardComponent, title: "Dashboard | JS" }, { path: 'basket', component: FundingBasketComponent, title: "Basket | JS" }, - { path: 'need/:id', component: NeedPageComponent, title: "Need | JS" } + { path: 'need/:id', component: NeedPageComponent, title: "Need | JS" }, + { path: 'signup', component: SignupComponent, title: "Signup | JS" }, ]; @NgModule({ diff --git a/ufund-ui/src/app/app.module.ts b/ufund-ui/src/app/app.module.ts index f1d6184..ea7e6ad 100644 --- a/ufund-ui/src/app/app.module.ts +++ b/ufund-ui/src/app/app.module.ts @@ -15,6 +15,7 @@ import {DashboardComponent} from './components/dashboard/dashboard.component'; import {CommonModule} from '@angular/common'; import {LoginComponent} from './components/login/login.component'; import { MiniNeedListComponent } from './components/mini-need-list/mini-need-list.component'; +import { SignupComponent } from './components/signup/signup.component'; @NgModule({ declarations: [ @@ -26,6 +27,7 @@ import { MiniNeedListComponent } from './components/mini-need-list/mini-need-lis NeedListComponent, DashboardComponent, LoginComponent, + SignupComponent, MiniNeedListComponent, ], imports: [ diff --git a/ufund-ui/src/app/components/login/login.component.html b/ufund-ui/src/app/components/login/login.component.html index e04ec23..743b1b3 100644 --- a/ufund-ui/src/app/components/login/login.component.html +++ b/ufund-ui/src/app/components/login/login.component.html @@ -5,8 +5,7 @@ <input placeholder="Password" type="password" #password> <button type="button" (click)="login(username.value, password.value)">Login</button> <div> - New? <a href="/">Create an account</a> + New? <a routerLink="/signup">Create an account</a> </div> -<!-- <button type="button" (click)="signup(username.value, password.value)">Create Account</button>--> <span *ngIf="statusText">{{statusText | async}}</span> </div> diff --git a/ufund-ui/src/app/components/signup/signup.component.css b/ufund-ui/src/app/components/signup/signup.component.css new file mode 100644 index 0000000..f286cf9 --- /dev/null +++ b/ufund-ui/src/app/components/signup/signup.component.css @@ -0,0 +1,47 @@ +:host { + display: flex; + flex-direction: column; + max-width: 300px; + gap: 10px; + + & > div { + display: flex; + flex-direction: column; + } +} + +.border { + border-style: solid; + border-width: 1px; + padding: 10px; + margin: 10px; + position: absolute; + background-color: white; + box-shadow: 0 0 10px 10px black; +} + +#bar { + height: 5px; + width: 100%; + appearance: none; + overflow: hidden; + /*margin-top: -5px;*/ +} + +#bar::-webkit-progress-bar { + background-color: lightgray; + transition: width 0.5s ease-in-out, background-color 0.5s ease-in-out; +} + +.color0::-webkit-progress-value { background: rgba(255, 0 ,0, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } +.color1::-webkit-progress-value { background: rgba(255, 0 ,0, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } +.color2::-webkit-progress-value { background: rgba(255, 165, 0, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } +.color3::-webkit-progress-value { background: rgba(255, 255, 0, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } +.color4::-webkit-progress-value { background: rgba(173, 255, 47, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } +.color5::-webkit-progress-value { background: rgba(50, 205, 50, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } +.color6::-webkit-progress-value { background: rgba(0, 128, 0, 1); transition: background-color 0.5s ease-in-out, width 0.5s ease-in-out; } + +#requirement2, #statusText, #passwordStatusText, #usernameStatusText { + color: red; +} + diff --git a/ufund-ui/src/app/components/signup/signup.component.html b/ufund-ui/src/app/components/signup/signup.component.html new file mode 100644 index 0000000..ebedc2a --- /dev/null +++ b/ufund-ui/src/app/components/signup/signup.component.html @@ -0,0 +1,26 @@ +<p>Signup:</p> +<div> + <input placeholder="Username" type="text" (input)="validate(username.value, confirmPass.value, password.value)" #username> + <span *ngIf="usernameStatusText">{{usernameStatusText | async}}</span> +</div> + +<div> + <input placeholder="Password" type="password" (input)="validate(username.value, confirmPass.value, password.value)" #password> + <progress [ngClass]="'color' + strength.getValue()" id="bar" [value]="strength | async" max="6"> </progress> + <span *ngIf="passwordStatusText">{{passwordStatusText | async}}</span> + + <span *ngFor="let requirement of Object.values(passwordRequirements)"> + <span *ngIf="passwordRequirements" [style.color]="requirement.value ? 'green' : 'red'"> {{requirement.title}} </span> + </span> +</div> + +<div> + <input placeholder="Confirm password" type="password" (input)="validate(username.value, confirmPass.value, password.value)" #confirmPass> + <span *ngIf="confirmPassStatusText">{{confirmPassStatusText | async}}</span> +</div> + +<div> + <button [disabled]="!(ableToCreateAccount | async)" (click)="signup(username.value, password.value)">Create Account</button> + <span *ngIf="showSuccessMessage | async">Account created <a routerLink="/login">Proceed to login</a></span> + <span *ngIf="statusText | async">{{statusText | async}}</span> +</div> diff --git a/ufund-ui/src/app/components/signup/signup.component.ts b/ufund-ui/src/app/components/signup/signup.component.ts new file mode 100644 index 0000000..3b43287 --- /dev/null +++ b/ufund-ui/src/app/components/signup/signup.component.ts @@ -0,0 +1,165 @@ +import {Component} from '@angular/core'; +import {UsersService} from '../../services/users.service'; +import {Router} from '@angular/router'; +import {BehaviorSubject} from 'rxjs'; + +class PasswordRequirements { + sixLong: {title: string, value: boolean} = {title: 'Is 6 characters or longer' , value: false} + twelveLong: {title: string, value: boolean} = {title: 'Is 12 characters or longer', value: false} + lowercase: {title: string, value: boolean} = {title: 'Includes lowercase letter' , value: false} + uppercase: {title: string, value: boolean} = {title: 'Includes uppercase letter' , value: false} + number: {title: string, value: boolean} = {title: 'Includes number' , value: false} + symbol: {title: string, value: boolean} = {title: 'Includes symbol' , value: false} +} + +@Component({ + selector: 'app-signup', + standalone: false, + templateUrl: './signup.component.html', + styleUrl: './signup.component.css' +}) + +export class SignupComponent { + + protected passwordStatusText = new BehaviorSubject("") + protected confirmPassStatusText = new BehaviorSubject("") + protected usernameStatusText = new BehaviorSubject("") + protected showSuccessMessage = new BehaviorSubject(false) + protected passwordStrongEnough = new BehaviorSubject(false) + protected ableToCreateAccount = new BehaviorSubject(false) + protected passwordRequirements: PasswordRequirements = new PasswordRequirements() + protected strength = new BehaviorSubject(0) + protected statusText = new BehaviorSubject(""); + + constructor( + protected usersService: UsersService, + protected router: Router, + ) {} + + signup(username: string | null, password: string | null) { + console.log(`attempting to sign up with ${username} ${password}`) + if (!username || !password) { + return; + } + + this.usersService.createUser(username, password).then(() => { + this.showSuccessMessage.next(true); + }).catch(ex => { + this.statusText.next("Unable to create account: " + friendlyHttpStatus[ex.status]) + console.log(ex) + }) + } + + validate(username: string, passConfirm:string, password: string) { + this.confirmPassStatusText.next("") + this.usernameStatusText.next("") + this.checkPasswordStrength(password); + + if (username === "") { + this.usernameStatusText.next("Username field can't be blank") + } + + if (!(password === passConfirm) && !!passConfirm) { + this.confirmPassStatusText.next("Passwords don't match") + } + this.ableToCreateAccount.next(this.passwordStrongEnough.getValue() && password === passConfirm && !!username) + } + + checkPasswordStrength(password: string) { + this.strength.next(0) + this.passwordRequirements = new PasswordRequirements() + this.passwordStrongEnough.next(false) + + if (password.match(/[^!-~]/g)) { + this.passwordStatusText.next("Invalid characters") + + return + } + + if (password.length > 6) { + this.passwordRequirements.sixLong.value = true + } + if (password.length > 12) { + this.passwordRequirements.twelveLong.value = true + } + if (password.match(/[a-z]/g)) { + this.passwordRequirements.lowercase.value = true + } + if (password.match(/[A-Z]/g)) { + this.passwordRequirements.uppercase.value = true + } + if (password.match(/[0-9]/g)) { + this.passwordRequirements.number.value = true + } + if (password.match(/[^A-Za-z0-9]/g)) { + this.passwordRequirements.symbol.value = true + } + + let strength = 0 + Object.values(this.passwordRequirements).forEach(k => { + k.value && strength++ + }) + + if (strength >= 5) { + this.passwordStrongEnough.next(true) + this.passwordStatusText.next("") + } else if (strength == 0) { + this.passwordStatusText.next("") + } else { + this.passwordStatusText.next("Password does not meet requirements") + } + + this.strength.next(strength) + } + + getColor() { + return `rgba(${(this.strength.getValue()/7) * 255}, ${255 - (this.strength.getValue()/7) * 255}, 0)` + } + + protected readonly length = length; + protected readonly Object = Object; +} + +let friendlyHttpStatus: {[key: number]: string} = { + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: 'Unused', + 307: 'Temporary Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Required', + 413: 'Request Entry Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m a teapot', + 429: 'Too Many Requests', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', +}; |