aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--ufund-api/src/main/java/com/ufund/api/ufundapi/controller/CupboardController.java2
-rw-r--r--ufund-api/src/test/java/com/ufund/api/ufundapi/controller/CupboardControllerTest.java137
-rw-r--r--ufund-ui/src/app/app-routing.module.ts4
-rw-r--r--ufund-ui/src/app/app.module.ts2
-rw-r--r--ufund-ui/src/app/components/login/login.component.html3
-rw-r--r--ufund-ui/src/app/components/signup/signup.component.css47
-rw-r--r--ufund-ui/src/app/components/signup/signup.component.html26
-rw-r--r--ufund-ui/src/app/components/signup/signup.component.ts165
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',
+};