Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

User management #766

Merged
merged 14 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions web/ui/dashboard/src/app/app-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ const routes: Routes = [
{
path: 'login',
loadChildren: () => import('./public/login/login.module').then(m => m.LoginModule)
},
{
path: 'forgot-password',
loadChildren: () => import('./public/forgot-password/forgot-password.module').then(m => m.ForgotPasswordModule)
},
{
path: 'reset-password',
loadChildren: () => import('./public/reset-password/reset-password.module').then(m => m.ResetPasswordModule)
},
{
path: 'accept-invite',
loadChildren: () => import('./public/accept-invite/accept-invite.module').then(m => m.AcceptInviteModule)
}
];

Expand Down
155 changes: 155 additions & 0 deletions web/ui/dashboard/src/app/private/pages/account/account.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<div class="page">
<div class="card flex padding-all__0px">
<div class="sidemenu">
<div class="border__bottom padding-x__24px padding-top__16px padding-bottom__10px">
<h3>User Settings</h3>
</div>
<ul class="sidemenu--items">
<li class="sidemenu--item" [ngClass]="{ active: activePage === 'profile' }">
<button class="button__clear" (click)="activePage = 'profile'">Profile</button>
</li>
<li class="sidemenu--item" [ngClass]="{ active: activePage === 'security' }">
<button class="button__clear" (click)="activePage = 'security'">Security</button>
</li>
<hr class="border__top" />
<li class="sidemenu--item font__14px color__danger font-weight__600" (click)="logout()">
<a href="">Logout</a>
</li>
</ul>
</div>

<div class="page__small--view">
<ng-container *ngIf="activePage === 'profile'">
<div class="flex flex__justify-between flex__align-items-center margin-bottom__28px">
<h3>Basic Info</h3>

<button class="button button__primary button__small" [disabled]="isSavingUserDetails" (click)="editBasicUserInfo()">{{ isSavingUserDetails ? 'Saving changes...' : 'Save Changes' }}</button>
</div>
<hr class="margin-bottom__22px" />

<form [formGroup]="editBasicInfoForm">
<div class="input smaller">
<label for="firstname">Firstname</label>
<input type="text" formControlName="first_name" id="firstname" required />
<div class="input__error input__error__danger" *ngIf="editBasicInfoForm.controls.first_name.touched && editBasicInfoForm.controls.first_name.invalid">
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Please enter your first name</span>
</div>
</div>
<div class="input smaller">
<label for="lastname">Lastname</label>
<input type="text" formControlName="last_name" id="lastname" required />
<div class="input__error input__error__danger" *ngIf="editBasicInfoForm.controls.last_name.touched && editBasicInfoForm.controls.last_name.invalid">
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Please enter your last name</span>
</div>
</div>

<div class="input">
<label for="email">Email Address</label>
<input type="email" formControlName="email" id="email" required />
<div class="input__error input__error__danger" *ngIf="editBasicInfoForm.controls.email.touched && editBasicInfoForm.controls.email.invalid">
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Please enter a valid email</span>
</div>
</div>
</form>
</ng-container>

<ng-container *ngIf="activePage === 'security'">
<div class="flex flex__justify-between flex__align-items-center margin-bottom__28px">
<h2 class="color__black">Change Password</h2>

<button class="button button__primary button__small " [disabled]="isUpdatingPassword" (click)="changePassword()">{{ isUpdatingPassword ? 'Saving changes...' : 'Save Changes' }}</button>
</div>
<form [formGroup]="changePasswordForm" (ngSubmit)="changePassword()">
<div class="input margin-bottom__0px">
<label for="current_password">Current Password</label>
<div class="input--password">
<input [type]="passwordToggle.oldPassword ? 'text' : 'password'" id="current_password" name="current_password" formControlName="current_password" />
<button class="input--password__view-toggle" (click)="passwordToggle.oldPassword = !passwordToggle.oldPassword" type="button">
<img
[src]="!passwordToggle.oldPassword ? '/assets/img/password-invisible-icon.svg' : '/assets/img/password-visible-icon.svg'"
[alt]="passwordToggle.oldPassword ? 'hide password icon' : 'view password icon'"
/>
</button>
</div>
<div class="input__error input__error__danger" *ngIf="changePasswordForm.controls.current_password.touched && changePasswordForm.controls.current_password.invalid">
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Enter current password</span>
</div>
</div>
<p class="color__black font__12px margin-top__8px">
Forgot password?
<button class="button__clear color__primary font__12px" type="button" [routerLink]="['/forgot-password']">Reset it here</button>
</p>
<div class="input margin-top__24px">
<label for="password">New Password</label>
<div class="input--password">
<input [type]="passwordToggle.newPassword ? 'text' : 'password'" id="password" name="password" formControlName="password" />
<button class="input--password__view-toggle" (click)="passwordToggle.newPassword = !passwordToggle.newPassword" type="button">
<img
[src]="!passwordToggle.newPassword ? '/assets/img/password-invisible-icon.svg' : '/assets/img/password-visible-icon.svg'"
[alt]="passwordToggle.newPassword ? 'hide password icon' : 'view password icon'"
/>
</button>
</div>
<div class="input__error input__error__danger" *ngIf="changePasswordForm.controls.password.touched && changePasswordForm.controls.password.invalid">
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Enter new password</span>
</div>
</div>
<div class="input">
<label for="password_confirmation">Confirm Password</label>
<div class="input--password">
<input
[type]="passwordToggle.confirmPassword ? 'text' : 'password'"
id="password_confirmation"
name="password_confirmation"
autocomplete="password_confirmation"
formControlName="password_confirmation"
[ngClass]="{
danger:
(changePasswordForm.controls.password_confirmation.touched && changePasswordForm.controls.password_confirmation.invalid) ||
(changePasswordForm.controls.password.valid &&
changePasswordForm.controls.password_confirmation.valid &&
changePasswordForm.controls.password_confirmation.touched &&
!checkPassword())
}"
/>
<button class="input--password__view-toggle" (click)="passwordToggle.confirmPassword = !passwordToggle.confirmPassword" type="button">
<img
[src]="!passwordToggle.confirmPassword ? '/assets/img/password-invisible-icon.svg' : '/assets/img/password-visible-icon.svg'"
[alt]="passwordToggle.confirmPassword ? 'hide password icon' : 'view password icon'"
/>
</button>
</div>
<div class="input__error input__error__danger" *ngIf="changePasswordForm.controls.password_confirmation.touched && changePasswordForm.controls.password_confirmation.invalid">
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Confirm new password</span>
</div>
<div
class="input__error input__error__danger"
*ngIf="
changePasswordForm.controls.password.valid &&
changePasswordForm.controls.password_confirmation.valid &&
changePasswordForm.controls.password_confirmation.touched &&
!checkPassword()
"
>
<img src="assets/img/input-error-icon.svg" alt="input error icon" />
<span>Passwords do not match</span>
</div>
</div>
</form>

<div class="alert alert__info width__100 width-unset-max">
<img src="/assets/img/ideas.svg" alt="idea" />
<div>
<p>Use a strong password with at least 8 characters long, and having a mixed of letters, numbers, and symbols.</p>
</div>
</div>
</ng-container>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.page {
margin-top: 80px;
max-width: calc(834px + 48px);
width: 100%;
}
.sidemenu {
border-right: 1px solid var(--line-border);
max-width: 260px;
width: 100%;

&--item {
margin: 30px 0;
padding-left: 22px;

button {
color: var(--grey-color);
}

&.active {
color: var(--primary-color);
position: relative;

&::before {
position: absolute;
content: '';
width: 3px;
height: 16px;
background-color: var(--primary-color);
left: 0;
top: 50%;
transform: translate(0, -50%);
border-radius: 100px;
}

button {
font-weight: 600;
color: var(--primary-color);
}
}
}
}

.alert__info img {
margin-right: 10px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { AccountComponent } from './account.component';

describe('AccountComponent', () => {
let component: AccountComponent;
let fixture: ComponentFixture<AccountComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AccountComponent ]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AccountComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
110 changes: 110 additions & 0 deletions web/ui/dashboard/src/app/private/pages/account/account.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { GeneralService } from 'src/app/services/general/general.service';
import { AccountService } from './account.service';

@Component({
selector: 'app-account',
templateUrl: './account.component.html',
styleUrls: ['./account.component.scss']
})
export class AccountComponent implements OnInit {
activePage: 'profile' | 'security' = 'profile';
isSavingUserDetails = false;
isUpdatingPassword = false;
isFetchingUserDetails = false;
userId!: string;
passwordToggle = { oldPassword: false, newPassword: false, confirmPassword: false };
editBasicInfoForm: FormGroup = this.formBuilder.group({
first_name: ['', Validators.required],
last_name: ['', Validators.required],
email: ['', Validators.compose([Validators.required, Validators.email])]
});
changePasswordForm: FormGroup = this.formBuilder.group({
current_password: ['', Validators.required],
password: ['', Validators.required],
password_confirmation: ['', Validators.required]
});
constructor(private accountService: AccountService, private router: Router, private formBuilder: FormBuilder, private generalService: GeneralService) {}

ngOnInit() {
const authDetails = localStorage.getItem('CONVOY_AUTH');
if (authDetails && authDetails !== 'undefined') {
const userId = JSON.parse(authDetails)?.uid;
this.getUserDetails(userId);
} else {
this.router.navigateByUrl('/login');
}
}

async getUserDetails(userId: string) {
this.isFetchingUserDetails = true;

try {
const response = await this.accountService.getUserDetails({ userId: userId });
this.userId = response.data?.uid;
this.editBasicInfoForm.patchValue({
first_name: response.data?.first_name,
last_name: response.data?.last_name,
email: response.data?.email
});
this.isFetchingUserDetails = false;
} catch {
this.isFetchingUserDetails = false;
}
}
async logout() {
await this.accountService.logout();
localStorage.removeItem('CONVOY_AUTH');
this.router.navigateByUrl('/login');
}

async editBasicUserInfo() {
if (this.editBasicInfoForm.invalid) {
(<any>Object).values(this.editBasicInfoForm.controls).forEach((control: FormControl) => {
control?.markAsTouched();
});
return;
}
this.isSavingUserDetails = true;
try {
const response = await this.accountService.editBasicInfo({ userId: this.userId, body: this.editBasicInfoForm.value });
this.generalService.showNotification({ style: 'success', message: 'Changes saved successfully!' });
this.getUserDetails(this.userId);
this.isSavingUserDetails = false;
} catch {
this.isSavingUserDetails = false;
}
}

async changePassword() {
if (this.changePasswordForm.invalid) {
(<any>Object).values(this.changePasswordForm.controls).forEach((control: FormControl) => {
control?.markAsTouched();
});
return;
}
this.isUpdatingPassword = true;
try {
const response = await this.accountService.changePassword({ userId: this.userId, body: this.changePasswordForm.value });
if (response.status === true) {
this.generalService.showNotification({ style: 'success', message: response.message });
this.changePasswordForm.reset();
}
this.isUpdatingPassword = false;
} catch {
this.isUpdatingPassword = false;
}
}

checkPassword(): boolean {
const newPassword = this.changePasswordForm.value.password;
const confirmPassword = this.changePasswordForm.value.password_confirmation;
if (newPassword === confirmPassword) {
return true;
} else {
return false;
}
}
}
13 changes: 13 additions & 0 deletions web/ui/dashboard/src/app/private/pages/account/account.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AccountComponent } from './account.component';
import { RouterModule, Routes } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';

const routes: Routes = [{ path: '', component: AccountComponent }];

@NgModule({
declarations: [AccountComponent],
imports: [CommonModule, ReactiveFormsModule, RouterModule.forChild(routes)]
})
export class AccountModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AccountService } from './account.service';

describe('AccountService', () => {
let service: AccountService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AccountService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
Loading