Account Email Support (#1000)
* Moved the Server Settings out into a button on nav header * Refactored Mange Users page to the new design (skeleton). Implemented skeleton code for Invite User. * Hashed out more of the code, but need to move all the email code to a Kavita controlled API server due to password credentials. * Cleaned up some warnings * When no user exists for an api key in Plugin controller, throw 401. * Hooked in the ability to check if the Kavita instance can be accessed externally so we can determine if the user can invite or not. * Hooked up some logic if the user's server isn't accessible, then default to old flow * Basic flow is working for confirm email. Needs validation, error handling, etc. * Refactored Password validation to account service * Cleaned up the code in confirm-email to work much better. * Refactored the login page to have a container functionality, so we can reuse the styles on multiple pages (registration pages). Hooked up the code for confirm email. * Messy code, but making progress. Refactored Register to be used only for first time user registration. Added a new register component to handle first time flow only. * Invite works much better, still needs a bit of work for non-accessible server setup. Started work on underlying manage users page to meet new design. * Changed (you) to a star to indicate who you're logged in as. * Inviting a user is now working and tested fully. * Removed the register member component as we now have invite and confirm components. * Editing a user is now working. Username change and Role/Library access from within one screen. Email changing is on hold. * Cleaned up code for edit user and disabled email field for now. * Cleaned up the code to indicate changing a user's email is not possible. * Implemented a migration for existing accounts so they can validate their emails and still login. * Change url for email server * Implemented the ability to resend an email confirmation code (or regenerate for non accessible servers). Fixed an overflow on the confirm dialog. * Took care of some code cleanup * Removed 3 db calls from cover refresh and some misc cleanup * Fixed a broken test
This commit is contained in:
parent
6e6b72a5b5
commit
efb527035d
109 changed files with 2041 additions and 407 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import { Library } from './library';
|
||||
|
||||
export interface Member {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
lastActive: string; // datetime
|
||||
created: string; // datetime
|
||||
isAdmin: boolean;
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class AccountService implements OnDestroy {
|
|||
return this.httpClient.get<string[]>(this.baseUrl + 'account/roles');
|
||||
}
|
||||
|
||||
login(model: any): Observable<any> {
|
||||
login(model: {username: string, password: string}): Observable<any> {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/login', model).pipe(
|
||||
map((response: User) => {
|
||||
const user = response;
|
||||
|
|
@ -91,25 +91,13 @@ export class AccountService implements OnDestroy {
|
|||
this.messageHub.stopHubConnection();
|
||||
}
|
||||
|
||||
// setCurrentUser() {
|
||||
// // TODO: Refactor this to setCurentUser in accoutnService
|
||||
// const user = this.getUserFromLocalStorage();
|
||||
|
||||
|
||||
// if (user) {
|
||||
// this.navService.setDarkMode(user.preferences.siteDarkMode);
|
||||
// this.messageHub.createHubConnection(user, this.accountService.hasAdminRole(user));
|
||||
// this.libraryService.getLibraryNames().pipe(take(1)).subscribe(() => {/* No Operation */});
|
||||
// } else {
|
||||
// this.navService.setDarkMode(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
register(model: {username: string, password: string, isAdmin?: boolean}) {
|
||||
if (!model.hasOwnProperty('isAdmin')) {
|
||||
model.isAdmin = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the first admin on the account. Only used for that. All other registrations must occur through invite
|
||||
* @param model
|
||||
* @returns
|
||||
*/
|
||||
register(model: {username: string, password: string, email: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/register', model).pipe(
|
||||
map((user: User) => {
|
||||
return user;
|
||||
|
|
@ -118,6 +106,26 @@ export class AccountService implements OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
migrateUser(model: {email: string, username: string, password: string, sendEmail: boolean}) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/migrate-email', model, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
confirmMigrationEmail(model: {email: string, token: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/confirm-migration-email', model);
|
||||
}
|
||||
|
||||
resendConfirmationEmail(userId: number) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, sendEmail: boolean}) {
|
||||
return this.httpClient.post<string>(this.baseUrl + 'account/invite', model, {responseType: 'text' as 'json'});
|
||||
}
|
||||
|
||||
confirmEmail(model: {email: string, username: string, password: string, token: string}) {
|
||||
return this.httpClient.post<User>(this.baseUrl + 'account/confirm-email', model);
|
||||
}
|
||||
|
||||
getDecodedToken(token: string) {
|
||||
return JSON.parse(atob(token.split('.')[1]));
|
||||
}
|
||||
|
|
@ -126,6 +134,10 @@ export class AccountService implements OnDestroy {
|
|||
return this.httpClient.post(this.baseUrl + 'account/reset-password', {username, password}, {responseType: 'json' as 'text'});
|
||||
}
|
||||
|
||||
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number}) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update', model);
|
||||
}
|
||||
|
||||
updatePreferences(userPreferences: Preferences) {
|
||||
return this.httpClient.post<Preferences>(this.baseUrl + 'users/update-preferences', userPreferences).pipe(map(settings => {
|
||||
if (this.currentUser !== undefined || this.currentUser != null) {
|
||||
|
|
|
|||
|
|
@ -39,4 +39,8 @@ export class MemberService {
|
|||
updateMemberRoles(username: string, roles: string[]) {
|
||||
return this.httpClient.post(this.baseUrl + 'account/update-rbs', {username, roles});
|
||||
}
|
||||
|
||||
getPendingInvites() {
|
||||
return this.httpClient.get<Array<Member>>(this.baseUrl + 'users/pending');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,4 +36,8 @@ export class ServerService {
|
|||
getChangelog() {
|
||||
return this.httpClient.get<UpdateVersionEvent[]>(this.baseUrl + 'server/changelog', {});
|
||||
}
|
||||
|
||||
isServerAccessible() {
|
||||
return this.httpClient.get<boolean>(this.baseUrl + 'server/accessible');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { Member } from 'src/app/_models/member';
|
|||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
|
||||
// TODO: Remove this component, edit-user will take over
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-rbs-modal',
|
||||
templateUrl: './edit-rbs-modal.component.html',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export class LibraryAccessModalComponent implements OnInit {
|
|||
isLoading: boolean = false;
|
||||
|
||||
get hasSomeSelected() {
|
||||
console.log(this.selections != null && this.selections.hasSomeSelected());
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ import { EditRbsModalComponent } from './_modals/edit-rbs-modal/edit-rbs-modal.c
|
|||
import { ManageSystemComponent } from './manage-system/manage-system.component';
|
||||
import { ChangelogComponent } from './changelog/changelog.component';
|
||||
import { PipeModule } from '../pipe/pipe.module';
|
||||
import { InviteUserComponent } from './invite-user/invite-user.component';
|
||||
import { RoleSelectorComponent } from './role-selector/role-selector.component';
|
||||
import { LibrarySelectorComponent } from './library-selector/library-selector.component';
|
||||
import { EditUserComponent } from './edit-user/edit-user.component';
|
||||
|
||||
|
||||
|
||||
|
|
@ -33,6 +37,10 @@ import { PipeModule } from '../pipe/pipe.module';
|
|||
EditRbsModalComponent,
|
||||
ManageSystemComponent,
|
||||
ChangelogComponent,
|
||||
InviteUserComponent,
|
||||
RoleSelectorComponent,
|
||||
LibrarySelectorComponent,
|
||||
EditUserComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
|
|
|||
58
UI/Web/src/app/admin/edit-user/edit-user.component.html
Normal file
58
UI/Web/src/app/admin/edit-user/edit-user.component.html
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Edit {{member.username | sentenceCase}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<form [formGroup]="userForm">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-6 col-sm-12 pr-2">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
||||
<div *ngIf="userForm.get('username')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="form-group" style="width:100%">
|
||||
<label for="email">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" [disabled]="true">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="userForm.dirty || userForm.touched">
|
||||
<div *ngIf="userForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="userForm.get('email')?.errors?.email">
|
||||
This must be a valid email address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true" [member]="member"></app-role-selector>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-library-selector (selected)="updateLibrarySelection($event)" [member]="member"></app-library-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="isSaving || !userForm.valid">
|
||||
<span *ngIf="isSaving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSaving ? 'Saving...' : 'Update'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
0
UI/Web/src/app/admin/edit-user/edit-user.component.scss
Normal file
0
UI/Web/src/app/admin/edit-user/edit-user.component.scss
Normal file
62
UI/Web/src/app/admin/edit-user/edit-user.component.ts
Normal file
62
UI/Web/src/app/admin/edit-user/edit-user.component.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
||||
// TODO: Rename this to EditUserModal
|
||||
@Component({
|
||||
selector: 'app-edit-user',
|
||||
templateUrl: './edit-user.component.html',
|
||||
styleUrls: ['./edit-user.component.scss']
|
||||
})
|
||||
export class EditUserComponent implements OnInit {
|
||||
|
||||
@Input() member!: Member;
|
||||
|
||||
selectedRoles: Array<string> = [];
|
||||
selectedLibraries: Array<number> = [];
|
||||
isSaving: boolean = false;
|
||||
|
||||
userForm: FormGroup = new FormGroup({});
|
||||
|
||||
public get email() { return this.userForm.get('email'); }
|
||||
public get username() { return this.userForm.get('username'); }
|
||||
public get password() { return this.userForm.get('password'); }
|
||||
|
||||
constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService,
|
||||
private confirmService: ConfirmService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.userForm.addControl('email', new FormControl(this.member.email, [Validators.required, Validators.email]));
|
||||
this.userForm.addControl('username', new FormControl(this.member.username, [Validators.required]));
|
||||
|
||||
this.userForm.get('email')?.disable();
|
||||
}
|
||||
|
||||
updateRoleSelection(roles: Array<string>) {
|
||||
this.selectedRoles = roles;
|
||||
}
|
||||
|
||||
updateLibrarySelection(libraries: Array<Library>) {
|
||||
this.selectedLibraries = libraries.map(l => l.id);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close(false);
|
||||
}
|
||||
|
||||
save() {
|
||||
const model = this.userForm.getRawValue();
|
||||
model.userId = this.member.id;
|
||||
model.roles = this.selectedRoles;
|
||||
model.libraries = this.selectedLibraries;
|
||||
this.accountService.update(model).subscribe(() => {
|
||||
this.modal.close(true);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
56
UI/Web/src/app/admin/invite-user/invite-user.component.html
Normal file
56
UI/Web/src/app/admin/invite-user/invite-user.component.html
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Invite User</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Invite a user to your server. Enter their email in and we will send them an email to create an account.
|
||||
</p>
|
||||
|
||||
<p *ngIf="!checkedAccessibility">
|
||||
<span class="spinner-border text-primary" style="width: 1.5rem; height: 1.5rem;" role="status" aria-hidden="true"></span>
|
||||
Checking accessibility of server...
|
||||
</p>
|
||||
|
||||
|
||||
<form [formGroup]="inviteForm">
|
||||
<div class="row no-gutters">
|
||||
<div class="form-group" style="width:100%">
|
||||
<label for="email">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="inviteForm.dirty || inviteForm.touched">
|
||||
<div *ngIf="email?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="emailLink !== '' && checkedAccessibility && !accessible">
|
||||
<p>Use this link to finish setting up the user account due to your server not being accessible outside your local network.</p>
|
||||
<a href="{{emailLink}}" target="_blank">{{emailLink}}</a>
|
||||
</ng-container>
|
||||
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-6">
|
||||
<app-role-selector (selected)="updateRoleSelection($event)" [allowAdmin]="true"></app-role-selector>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<app-library-selector (selected)="updateLibrarySelection($event)"></app-library-selector>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="invite()" [disabled]="isSending || !inviteForm.valid || !checkedAccessibility || emailLink !== ''">
|
||||
<span *ngIf="isSending" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>{{isSending ? 'Inviting...' : 'Invite'}}</span>
|
||||
</button>
|
||||
</div>
|
||||
80
UI/Web/src/app/admin/invite-user/invite-user.component.ts
Normal file
80
UI/Web/src/app/admin/invite-user/invite-user.component.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-invite-user',
|
||||
templateUrl: './invite-user.component.html',
|
||||
styleUrls: ['./invite-user.component.scss']
|
||||
})
|
||||
export class InviteUserComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Maintains if the backend is sending an email
|
||||
*/
|
||||
isSending: boolean = false;
|
||||
inviteForm: FormGroup = new FormGroup({});
|
||||
/**
|
||||
* If a user would be able to load this server up externally
|
||||
*/
|
||||
accessible: boolean = true;
|
||||
checkedAccessibility: boolean = false;
|
||||
selectedRoles: Array<string> = [];
|
||||
selectedLibraries: Array<number> = [];
|
||||
emailLink: string = '';
|
||||
|
||||
public get email() { return this.inviteForm.get('email'); }
|
||||
|
||||
constructor(public modal: NgbActiveModal, private accountService: AccountService, private serverService: ServerService,
|
||||
private confirmService: ConfirmService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.inviteForm.addControl('email', new FormControl('', [Validators.required]));
|
||||
|
||||
this.serverService.isServerAccessible().subscribe(async (accessibile) => {
|
||||
if (!accessibile) {
|
||||
await this.confirmService.alert('This server is not accessible outside the network. You cannot invite via Email. You wil be given a link to finish registration with instead.');
|
||||
this.accessible = accessibile;
|
||||
}
|
||||
this.checkedAccessibility = true;
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close(false);
|
||||
}
|
||||
|
||||
invite() {
|
||||
|
||||
this.isSending = true;
|
||||
const email = this.inviteForm.get('email')?.value;
|
||||
this.accountService.inviteUser({
|
||||
email,
|
||||
libraries: this.selectedLibraries,
|
||||
roles: this.selectedRoles,
|
||||
sendEmail: this.accessible
|
||||
}).subscribe(email => {
|
||||
this.emailLink = email;
|
||||
this.isSending = false;
|
||||
if (this.accessible) {
|
||||
this.modal.close(true);
|
||||
}
|
||||
}, err => {
|
||||
this.isSending = false;
|
||||
});
|
||||
}
|
||||
|
||||
updateRoleSelection(roles: Array<string>) {
|
||||
this.selectedRoles = roles;
|
||||
}
|
||||
|
||||
updateLibrarySelection(libraries: Array<Library>) {
|
||||
this.selectedLibraries = libraries.map(l => l.id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<h4>Libraries</h4>
|
||||
<div class="list-group" *ngIf="!isLoading">
|
||||
<div class="form-check" *ngIf="allLibraries.length > 0">
|
||||
<input id="selectall" type="checkbox" class="form-check-input"
|
||||
[ngModel]="selectAll" (change)="toggleAll()" [indeterminate]="hasSomeSelected">
|
||||
<label for="selectall" class="form-check-label">{{selectAll ? 'Deselect' : 'Select'}} All</label>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="list-group-item" *ngFor="let library of allLibraries; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="library-{{i}}" type="checkbox" class="form-check-input" attr.aria-label="Library {{library.name}}"
|
||||
[ngModel]="selections.isSelected(library)" (change)="handleSelection(library)">
|
||||
<label attr.for="library-{{i}}" class="form-check-label">{{library.name}}</label>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="allLibraries.length === 0">
|
||||
There are no libraries setup yet.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.list-group-item {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { SelectionModel } from 'src/app/typeahead/typeahead.component';
|
||||
import { Library } from 'src/app/_models/library';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { LibraryService } from 'src/app/_services/library.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-library-selector',
|
||||
templateUrl: './library-selector.component.html',
|
||||
styleUrls: ['./library-selector.component.scss']
|
||||
})
|
||||
export class LibrarySelectorComponent implements OnInit {
|
||||
|
||||
@Input() member: Member | undefined;
|
||||
@Output() selected: EventEmitter<Array<Library>> = new EventEmitter<Array<Library>>();
|
||||
|
||||
allLibraries: Library[] = [];
|
||||
selectedLibraries: Array<{selected: boolean, data: Library}> = [];
|
||||
selections!: SelectionModel<Library>;
|
||||
selectAll: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
|
||||
get hasSomeSelected() {
|
||||
return this.selections != null && this.selections.hasSomeSelected();
|
||||
}
|
||||
|
||||
constructor(private libraryService: LibraryService, private fb: FormBuilder) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.libraryService.getLibraries().subscribe(libs => {
|
||||
this.allLibraries = libs;
|
||||
this.setupSelections();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setupSelections() {
|
||||
this.selections = new SelectionModel<Library>(false, this.allLibraries);
|
||||
this.isLoading = false;
|
||||
|
||||
// If a member is passed in, then auto-select their libraries
|
||||
if (this.member !== undefined) {
|
||||
this.member.libraries.forEach(lib => {
|
||||
this.selections.toggle(lib, true, (a, b) => a.name === b.name);
|
||||
});
|
||||
this.selectAll = this.selections.selected().length === this.allLibraries.length;
|
||||
this.selected.emit(this.selections.selected());
|
||||
}
|
||||
}
|
||||
|
||||
toggleAll() {
|
||||
this.selectAll = !this.selectAll;
|
||||
this.allLibraries.forEach(s => this.selections.toggle(s, this.selectAll));
|
||||
this.selected.emit(this.selections.selected());
|
||||
}
|
||||
|
||||
handleSelection(item: Library) {
|
||||
this.selections.toggle(item);
|
||||
const numberOfSelected = this.selections.selected().length;
|
||||
if (numberOfSelected == 0) {
|
||||
this.selectAll = false;
|
||||
} else if (numberOfSelected == this.selectedLibraries.length) {
|
||||
this.selectAll = true;
|
||||
}
|
||||
|
||||
this.selected.emit(this.selections.selected());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,19 +1,53 @@
|
|||
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Users</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-right" (click)="createMember()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Add User</span></button></div>
|
||||
</div>
|
||||
<ul class="list-group" *ngIf="!createMemberToggle; else createUser">
|
||||
<ng-container>
|
||||
<div class="row mb-2">
|
||||
<div class="col-8"><h3>Pending Invites</h3></div>
|
||||
<div class="col-4"><button class="btn btn-primary float-right" (click)="inviteUser()"><i class="fa fa-plus" aria-hidden="true"></i><span class="phone-hidden"> Invite</span></button></div>
|
||||
</div>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" *ngFor="let invite of pendingInvites; let idx = index;">
|
||||
<div>
|
||||
<h4>
|
||||
<span id="member-name--{{idx}}">{{invite.username | titlecase}} </span>
|
||||
<div class="float-right">
|
||||
<button class="btn btn-danger mr-2" (click)="deleteUser(invite)">Cancel</button>
|
||||
<button class="btn btn-secondary mr-2" (click)="resendEmail(invite)">Resend</button>
|
||||
</div>
|
||||
</h4>
|
||||
|
||||
<div>Invited: {{invite.created | date: 'short'}}</div>
|
||||
</div>
|
||||
</li>
|
||||
<li *ngIf="loadingMembers" class="list-group-item">
|
||||
<div class="spinner-border text-secondary" role="status">
|
||||
<span class="invisible">Loading...</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item" *ngIf="pendingInvites.length === 0 && !loadingMembers">
|
||||
There are no invited Users
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<h3 class="mt-3">Active Users</h3>
|
||||
<ul class="list-group">
|
||||
<li *ngFor="let member of members; let idx = index;" class="list-group-item">
|
||||
<div>
|
||||
<h4>
|
||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.username)"></i><span id="member-name--{{idx}}">{{member.username | titlecase}} </span><span *ngIf="member.username === loggedInUsername">(You)</span>
|
||||
<i class="presence fa fa-circle" title="Active" aria-hidden="true" *ngIf="false && (messageHub.onlineUsers$ | async)?.includes(member.username)"></i>
|
||||
<span id="member-name--{{idx}}">{{member.username | titlecase}} </span>
|
||||
<span *ngIf="member.username === loggedInUsername">
|
||||
<i class="fas fa-star" aria-hidden="true"></i>
|
||||
<span class="sr-only">(You)</span>
|
||||
</span>
|
||||
<div class="float-right" *ngIf="canEditMember(member)">
|
||||
<button class="btn btn-danger mr-2" (click)="deleteUser(member)" placement="top" ngbTooltip="Delete User" attr.aria-label="Delete User {{member.username | titlecase}}"><i class="fa fa-trash" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-secondary mr-2" (click)="updatePassword(member)" placement="top" ngbTooltip="Change Password" attr.aria-label="Change Password for {{member.username | titlecase}}"><i class="fa fa-key" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-primary" (click)="openEditLibraryAccess(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
||||
<button class="btn btn-primary" (click)="openEditUser(member)" placement="top" ngbTooltip="Edit" attr.aria-label="Edit {{member.username | titlecase}}"><i class="fa fa-pen" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</h4>
|
||||
<div>Last Active:
|
||||
|
|
@ -44,7 +78,4 @@
|
|||
There are no other users.
|
||||
</li>
|
||||
</ul>
|
||||
<ng-template #createUser>
|
||||
<app-register-member (created)="onMemberCreated($event)"></app-register-member>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
|
@ -5,13 +5,15 @@ import { MemberService } from 'src/app/_services/member.service';
|
|||
import { Member } from 'src/app/_models/member';
|
||||
import { User } from 'src/app/_models/user';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { LibraryAccessModalComponent } from '../_modals/library-access-modal/library-access-modal.component';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ResetPasswordModalComponent } from '../_modals/reset-password-modal/reset-password-modal.component';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { EditRbsModalComponent } from '../_modals/edit-rbs-modal/edit-rbs-modal.component';
|
||||
import { Subject } from 'rxjs';
|
||||
import { MessageHubService } from 'src/app/_services/message-hub.service';
|
||||
import { InviteUserComponent } from '../invite-user/invite-user.component';
|
||||
import { EditUserComponent } from '../edit-user/edit-user.component';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-users',
|
||||
|
|
@ -21,10 +23,8 @@ import { MessageHubService } from 'src/app/_services/message-hub.service';
|
|||
export class ManageUsersComponent implements OnInit, OnDestroy {
|
||||
|
||||
members: Member[] = [];
|
||||
pendingInvites: Member[] = [];
|
||||
loggedInUsername = '';
|
||||
|
||||
// Create User functionality
|
||||
createMemberToggle = false;
|
||||
loadingMembers = false;
|
||||
|
||||
private onDestroy = new Subject<void>();
|
||||
|
|
@ -34,7 +34,8 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
private modalService: NgbModal,
|
||||
private toastr: ToastrService,
|
||||
private confirmService: ConfirmService,
|
||||
public messageHub: MessageHubService) {
|
||||
public messageHub: MessageHubService,
|
||||
private serverService: ServerService) {
|
||||
this.accountService.currentUser$.pipe(take(1)).subscribe((user: User) => {
|
||||
this.loggedInUsername = user.username;
|
||||
});
|
||||
|
|
@ -43,6 +44,8 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.loadMembers();
|
||||
|
||||
this.loadPendingInvites();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
|
|
@ -69,31 +72,41 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
loadPendingInvites() {
|
||||
this.memberService.getPendingInvites().subscribe(members => {
|
||||
this.pendingInvites = members;
|
||||
// Show logged in user at the top of the list
|
||||
this.pendingInvites.sort((a: Member, b: Member) => {
|
||||
if (a.username === this.loggedInUsername) return 1;
|
||||
if (b.username === this.loggedInUsername) return 1;
|
||||
|
||||
const nameA = a.username.toUpperCase();
|
||||
const nameB = b.username.toUpperCase();
|
||||
if (nameA < nameB) return -1;
|
||||
if (nameA > nameB) return 1;
|
||||
return 0;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
canEditMember(member: Member): boolean {
|
||||
return this.loggedInUsername !== member.username;
|
||||
}
|
||||
|
||||
createMember() {
|
||||
this.createMemberToggle = true;
|
||||
}
|
||||
|
||||
onMemberCreated(createdUser: User | null) {
|
||||
this.createMemberToggle = false;
|
||||
this.loadMembers();
|
||||
}
|
||||
|
||||
openEditLibraryAccess(member: Member) {
|
||||
const modalRef = this.modalService.open(LibraryAccessModalComponent);
|
||||
openEditUser(member: Member) {
|
||||
const modalRef = this.modalService.open(EditUserComponent, {size: 'lg'});
|
||||
modalRef.componentInstance.member = member;
|
||||
modalRef.closed.subscribe(() => {
|
||||
this.loadMembers();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async deleteUser(member: Member) {
|
||||
if (await this.confirmService.confirm('Are you sure you want to delete this user?')) {
|
||||
this.memberService.deleteMember(member.username).subscribe(() => {
|
||||
this.loadMembers();
|
||||
this.loadPendingInvites();
|
||||
this.toastr.success(member.username + ' has been deleted.');
|
||||
});
|
||||
}
|
||||
|
|
@ -106,7 +119,32 @@ export class ManageUsersComponent implements OnInit, OnDestroy {
|
|||
if (updatedMember !== undefined) {
|
||||
member = updatedMember;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
inviteUser() {
|
||||
const modalRef = this.modalService.open(InviteUserComponent, {size: 'lg'});
|
||||
modalRef.closed.subscribe((successful: boolean) => {
|
||||
if (successful) {
|
||||
this.loadPendingInvites();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resendEmail(member: Member) {
|
||||
|
||||
this.serverService.isServerAccessible().subscribe(canAccess => {
|
||||
this.accountService.resendConfirmationEmail(member.id).subscribe(async (email) => {
|
||||
if (canAccess) {
|
||||
this.toastr.info('Email sent to ' + member.username);
|
||||
return;
|
||||
}
|
||||
await this.confirmService.alert(
|
||||
'Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking. <br/> <a href="' + email + '" target="_blank">' + email + '</a>');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
updatePassword(member: Member) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<h4>Roles</h4>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" *ngFor="let role of selectedRoles; let i = index">
|
||||
<div class="form-check">
|
||||
<input id="role-{{i}}" type="checkbox" attr.aria-label="Role {{role.data}}" class="form-check-input"
|
||||
[(ngModel)]="role.selected" name="role" (ngModelChange)="handleModelUpdate()">
|
||||
<label attr.for="role-{{i}}" class="form-check-label">{{role.data}}</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.list-group-item {
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Member } from 'src/app/_models/member';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-role-selector',
|
||||
templateUrl: './role-selector.component.html',
|
||||
styleUrls: ['./role-selector.component.scss']
|
||||
})
|
||||
export class RoleSelectorComponent implements OnInit {
|
||||
|
||||
@Input() member: Member | undefined;
|
||||
/**
|
||||
* Allows the selection of Admin role
|
||||
*/
|
||||
@Input() allowAdmin: boolean = false;
|
||||
@Output() selected: EventEmitter<string[]> = new EventEmitter<string[]>();
|
||||
|
||||
allRoles: string[] = [];
|
||||
selectedRoles: Array<{selected: boolean, data: string}> = [];
|
||||
|
||||
constructor(public modal: NgbActiveModal, private accountService: AccountService, private memberService: MemberService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.getRoles().subscribe(roles => {
|
||||
let bannedRoles = ['Pleb'];
|
||||
if (!this.allowAdmin) {
|
||||
bannedRoles.push('Admin');
|
||||
}
|
||||
roles = roles.filter(item => !bannedRoles.includes(item));
|
||||
this.allRoles = roles;
|
||||
this.selectedRoles = roles.map(item => {
|
||||
return {selected: false, data: item};
|
||||
});
|
||||
this.selected.emit(this.selectedRoles.filter(item => item.selected).map(item => item.data));
|
||||
this.preselect();
|
||||
});
|
||||
}
|
||||
|
||||
preselect() {
|
||||
if (this.member !== undefined) {
|
||||
this.member.roles.forEach(role => {
|
||||
const foundRole = this.selectedRoles.filter(item => item.data === role);
|
||||
if (foundRole.length > 0) {
|
||||
foundRole[0].selected = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleModelUpdate() {
|
||||
this.selected.emit(this.selectedRoles.filter(item => item.selected).map(item => item.data));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -65,7 +65,11 @@ const routes: Routes = [
|
|||
|
||||
]
|
||||
},
|
||||
{path: 'login', component: UserLoginComponent},
|
||||
{
|
||||
path: 'registration',
|
||||
loadChildren: () => import('../app/registration/registration.module').then(m => m.RegistrationModule)
|
||||
},
|
||||
{path: 'login', component: UserLoginComponent}, // TODO: move this to registration module
|
||||
{path: 'no-connection', component: NotConnectedComponent},
|
||||
{path: '**', component: UserLoginComponent, pathMatch: 'full'}
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<nav class="navbar navbar-expand-md navbar-dark fixed-top" *ngIf="navService?.navbarVisible$ | async">
|
||||
<div class="container-fluid">
|
||||
<a class="sr-only sr-only-focusable focus-visible" href="javascript:void(0);" (click)="moveFocus()">Skip to main content</a>
|
||||
<a class="navbar-brand" routerLink="/library" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="phone-hidden"> Kavita</span></a>
|
||||
<a class="navbar-brand dark-exempt" routerLink="/library" routerLinkActive="active"><img class="logo" src="../../assets/images/logo.png" alt="kavita icon" aria-hidden="true"/><span class="phone-hidden"> Kavita</span></a>
|
||||
<ul class="navbar-nav col mr-auto">
|
||||
|
||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
||||
|
|
@ -62,17 +62,25 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-item" *ngIf="(accountService.currentUser$ | async) as user">
|
||||
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
|
||||
</div>
|
||||
<ng-container *ngIf="(accountService.currentUser$ | async) as user">
|
||||
<div class="nav-item">
|
||||
<app-nav-events-toggle [user]="user"></app-nav-events-toggle>
|
||||
</div>
|
||||
<div class="nav-item pr-2">
|
||||
<a routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')" class="dark-exempt" style="padding: 5px">
|
||||
<i class="fa fa-cogs" aria-hidden="true" style="color: white"></i>
|
||||
<span class="sr-only">Server Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<div ngbDropdown class="nav-item dropdown" display="dynamic" placement="bottom-right" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
|
||||
<button class="btn btn-outline-secondary primary-text" ngbDropdownToggle>
|
||||
{{user.username | sentenceCase}}
|
||||
</button>
|
||||
<div ngbDropdownMenu>
|
||||
<a ngbDropdownItem routerLink="/preferences/">User Settings</a>
|
||||
<a ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</a>
|
||||
<a ngbDropdownItem routerLink="/preferences/">Settings</a>
|
||||
<a ngbDropdownItem (click)="logout()">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
|
||||
<div class="text-danger" *ngIf="errors.length > 0">
|
||||
<p>Errors:</p>
|
||||
<ul>
|
||||
<li *ngFor="let error of errors">{{error}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<form [formGroup]="registerForm" (ngSubmit)="register()">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text">
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngIf="registerForm.get('isAdmin')?.value || !authDisabled">
|
||||
<label for="password">Password</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #passwordTooltip>
|
||||
Password must be between 6 and 32 characters in length
|
||||
</ng-template>
|
||||
<span class="sr-only" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
|
||||
<input id="password" class="form-control" formControlName="password" type="password" aria-describedby="password-help">
|
||||
</div>
|
||||
|
||||
<div class="form-check" *ngIf="!firstTimeFlow">
|
||||
<input id="admin" type="checkbox" aria-label="Admin" class="form-check-input" formControlName="isAdmin">
|
||||
<label for="admin" class="form-check-label">Admin</label>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<button class="btn btn-secondary mr-2" type="button" (click)="cancel()" *ngIf="!firstTimeFlow">Cancel</button>
|
||||
<button class="btn btn-primary {{firstTimeFlow ? 'alt' : ''}}" type="submit">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
.alt {
|
||||
background-color: #424c72;
|
||||
border-color: #444f75;
|
||||
}
|
||||
|
||||
.alt:hover {
|
||||
background-color: #3b4466;
|
||||
}
|
||||
|
||||
.alt:focus {
|
||||
background-color: #343c59;
|
||||
box-shadow: 0 0 0 0.2rem rgb(68 79 117 / 50%);
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #fff !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { SettingsService } from '../admin/settings.service';
|
||||
import { User } from '../_models/user';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register-member',
|
||||
templateUrl: './register-member.component.html',
|
||||
styleUrls: ['./register-member.component.scss']
|
||||
})
|
||||
export class RegisterMemberComponent implements OnInit {
|
||||
|
||||
@Input() firstTimeFlow = false;
|
||||
/**
|
||||
* Emits the new user created.
|
||||
*/
|
||||
@Output() created = new EventEmitter<User | null>();
|
||||
|
||||
adminExists = false;
|
||||
authDisabled: boolean = false;
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
username: new FormControl('', [Validators.required]),
|
||||
password: new FormControl('', []),
|
||||
isAdmin: new FormControl(false, [])
|
||||
});
|
||||
errors: string[] = [];
|
||||
|
||||
constructor(private accountService: AccountService, private settingsService: SettingsService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.settingsService.getAuthenticationEnabled().pipe(take(1)).subscribe(authEnabled => {
|
||||
this.authDisabled = !authEnabled;
|
||||
});
|
||||
if (this.firstTimeFlow) {
|
||||
this.registerForm.get('isAdmin')?.setValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
register() {
|
||||
this.accountService.register(this.registerForm.value).subscribe(user => {
|
||||
this.created.emit(user);
|
||||
}, err => {
|
||||
this.errors = err;
|
||||
});
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.created.emit(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-basic-title">Account Migration</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="close()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Your account does not have an email on file. This is a one-time migration. Please add your email to the account. A verficiation link will be sent to your email for you
|
||||
to confirm and will then be allowed to authenticate with this server. This is required.
|
||||
</p>
|
||||
<form [formGroup]="registerForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('username')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="width:100%">
|
||||
<label for="email">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="registerForm.get('email')?.errors?.email">
|
||||
This must be a valid email address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('password')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="close()">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="isSaving || !registerForm.valid">
|
||||
<span *ngIf="isSaving" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
<span>Submit</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { SafeUrl } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfirmService } from 'src/app/shared/confirm.service';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
import { ServerService } from 'src/app/_services/server.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-email-to-account-migration-modal',
|
||||
templateUrl: './add-email-to-account-migration-modal.component.html',
|
||||
styleUrls: ['./add-email-to-account-migration-modal.component.scss']
|
||||
})
|
||||
export class AddEmailToAccountMigrationModalComponent implements OnInit {
|
||||
|
||||
@Input() username!: string;
|
||||
@Input() password!: string;
|
||||
|
||||
isSaving: boolean = false;
|
||||
registerForm: FormGroup = new FormGroup({});
|
||||
emailLink: string = '';
|
||||
emailLinkUrl: SafeUrl | undefined;
|
||||
|
||||
constructor(private accountService: AccountService, private modal: NgbActiveModal,
|
||||
private serverService: ServerService, private confirmService: ConfirmService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.registerForm.addControl('username', new FormControl(this.username, [Validators.required]));
|
||||
this.registerForm.addControl('email', new FormControl('', [Validators.required, Validators.email]));
|
||||
this.registerForm.addControl('password', new FormControl(this.password, [Validators.required]));
|
||||
}
|
||||
|
||||
close() {
|
||||
this.modal.close(false);
|
||||
}
|
||||
|
||||
save() {
|
||||
this.serverService.isServerAccessible().subscribe(canAccess => {
|
||||
const model = this.registerForm.getRawValue();
|
||||
model.sendEmail = canAccess;
|
||||
this.accountService.migrateUser(model).subscribe(async (email) => {
|
||||
if (!canAccess) {
|
||||
// Display the email to the user
|
||||
this.emailLink = email;
|
||||
await this.confirmService.alert('Please click this link to confirm your email. You must confirm to be able to login. You may need to log out of the current account before clicking. <br/> <a href="' + this.emailLink + '" target="_blank">' + this.emailLink + '</a>');
|
||||
} else {
|
||||
await this.confirmService.alert('Please check your email for the confirmation link. You must confirm to be able to login.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<!--
|
||||
<div class="text-danger" *ngIf="errors.length > 0">
|
||||
<p>Errors:</p>
|
||||
<ul>
|
||||
<li *ngFor="let error of errors">{{error}}</li>
|
||||
</ul>
|
||||
</div> -->
|
||||
<app-splash-container>
|
||||
<ng-container title><h2>Register</h2></ng-container>
|
||||
<ng-container body>
|
||||
<p>Complete the form to complete your registration</p>
|
||||
<form [formGroup]="registerForm" (ngSubmit)="submit()">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('username')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="width:100%">
|
||||
<label for="email">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="registerForm.get('email')?.errors?.email">
|
||||
This must be a valid email address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #passwordTooltip>
|
||||
Password must be between 6 and 32 characters in length
|
||||
</ng-template>
|
||||
<span class="sr-only" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
|
||||
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('password')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="registerForm.get('password')?.errors?.minlength || registerForm.get('password')?.errors?.maxLength">
|
||||
Password must be between 6 and 32 characters in length
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<button class="btn btn-secondary alt" type="submit">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</app-splash-container>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
input {
|
||||
background-color: #fff !important;
|
||||
color: black;
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { FormControl, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-email',
|
||||
templateUrl: './confirm-email.component.html',
|
||||
styleUrls: ['./confirm-email.component.scss']
|
||||
})
|
||||
export class ConfirmEmailComponent implements OnInit {
|
||||
|
||||
|
||||
/**
|
||||
* Email token used for validating
|
||||
*/
|
||||
token: string = '';
|
||||
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
email: new FormControl('', [Validators.required, Validators.email]),
|
||||
username: new FormControl('', [Validators.required]),
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6)]),
|
||||
});
|
||||
|
||||
/**
|
||||
* Validation errors from API
|
||||
*/
|
||||
errors: Array<string> = [];
|
||||
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService) {
|
||||
|
||||
const token = this.route.snapshot.queryParamMap.get('token');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
if (token == undefined || token === '' || token === null) {
|
||||
// This is not a valid url, redirect to login
|
||||
this.toastr.error('Invalid confirmation email');
|
||||
this.router.navigateByUrl('login');
|
||||
return;
|
||||
}
|
||||
this.token = token;
|
||||
this.registerForm.get('email')?.setValue(email || '');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
submit() {
|
||||
let model = this.registerForm.getRawValue();
|
||||
model.token = this.token;
|
||||
this.accountService.confirmEmail(model).subscribe((user) => {
|
||||
this.toastr.success('Account registration complete');
|
||||
this.router.navigateByUrl('login');
|
||||
}, err => {
|
||||
this.errors = err;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirm-migration-email',
|
||||
templateUrl: './confirm-migration-email.component.html',
|
||||
styleUrls: ['./confirm-migration-email.component.scss']
|
||||
})
|
||||
export class ConfirmMigrationEmailComponent implements OnInit {
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService) {
|
||||
|
||||
const token = this.route.snapshot.queryParamMap.get('token');
|
||||
const email = this.route.snapshot.queryParamMap.get('email');
|
||||
if (token === undefined || token === '' || token === null || email === undefined || email === '' || email === null) {
|
||||
// This is not a valid url, redirect to login
|
||||
this.toastr.error('Invalid confirmation email');
|
||||
this.router.navigateByUrl('login');
|
||||
return;
|
||||
}
|
||||
this.accountService.confirmMigrationEmail({token: token, email}).subscribe((user) => {
|
||||
this.toastr.success('Account migration complete');
|
||||
this.router.navigateByUrl('login');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
58
UI/Web/src/app/registration/register/register.component.html
Normal file
58
UI/Web/src/app/registration/register/register.component.html
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<!--
|
||||
<div class="text-danger" *ngIf="errors.length > 0">
|
||||
<p>Errors:</p>
|
||||
<ul>
|
||||
<li *ngFor="let error of errors">{{error}}</li>
|
||||
</ul>
|
||||
</div> -->
|
||||
<app-splash-container>
|
||||
<ng-container title><h2>Register</h2></ng-container>
|
||||
<ng-container body>
|
||||
<p>Complete the form to register an admin account</p>
|
||||
<form [formGroup]="registerForm" (ngSubmit)="submit()">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" class="form-control" formControlName="username" type="text">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('username')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="width:100%">
|
||||
<label for="email">Email</label>
|
||||
<input class="form-control" type="email" id="email" formControlName="email" required>
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('email')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="registerForm.get('email')?.errors?.email">
|
||||
This must be a valid email address
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="passwordTooltip" role="button" tabindex="0"></i>
|
||||
<ng-template #passwordTooltip>
|
||||
Password must be between 6 and 32 characters in length
|
||||
</ng-template>
|
||||
<span class="sr-only" id="password-help"><ng-container [ngTemplateOutlet]="passwordTooltip"></ng-container></span>
|
||||
<input id="password" class="form-control" maxlength="32" minlength="6" formControlName="password" type="password" aria-describedby="password-help">
|
||||
<div id="inviteForm-validations" class="invalid-feedback" *ngIf="registerForm.dirty || registerForm.touched">
|
||||
<div *ngIf="registerForm.get('password')?.errors?.required">
|
||||
This field is required
|
||||
</div>
|
||||
<div *ngIf="registerForm.get('password')?.errors?.minlength || registerForm.get('password')?.errors?.maxLength">
|
||||
Password must be between 6 and 32 characters in length
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float-right">
|
||||
<button class="btn btn-secondary alt" type="submit">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</app-splash-container>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
input {
|
||||
background-color: #fff !important;
|
||||
color: black;
|
||||
}
|
||||
46
UI/Web/src/app/registration/register/register.component.ts
Normal file
46
UI/Web/src/app/registration/register/register.component.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AccountService } from 'src/app/_services/account.service';
|
||||
import { MemberService } from 'src/app/_services/member.service';
|
||||
|
||||
/**
|
||||
* This is exclusivly used to register the first user on the server and nothing else
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
templateUrl: './register.component.html',
|
||||
styleUrls: ['./register.component.scss']
|
||||
})
|
||||
export class RegisterComponent implements OnInit {
|
||||
|
||||
registerForm: FormGroup = new FormGroup({
|
||||
email: new FormControl('', [Validators.required, Validators.email]),
|
||||
username: new FormControl('', [Validators.required]),
|
||||
password: new FormControl('', [Validators.required, Validators.maxLength(32), Validators.minLength(6)]),
|
||||
});
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router, private accountService: AccountService, private toastr: ToastrService, private memberService: MemberService) {
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
if (adminExists) {
|
||||
this.router.navigateByUrl('login');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
submit() {
|
||||
const model = this.registerForm.getRawValue();
|
||||
this.accountService.register(model).subscribe((user) => {
|
||||
this.toastr.success('Account registration complete');
|
||||
this.router.navigateByUrl('login');
|
||||
}, err => {
|
||||
// TODO: Handle errors
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
29
UI/Web/src/app/registration/registration.module.ts
Normal file
29
UI/Web/src/app/registration/registration.module.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ConfirmEmailComponent } from './confirm-email/confirm-email.component';
|
||||
import { RegistrationRoutingModule } from './registration.router.module';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { SplashContainerComponent } from './splash-container/splash-container.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
import { AddEmailToAccountMigrationModalComponent } from './add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
||||
import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confirm-migration-email.component';
|
||||
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
ConfirmEmailComponent,
|
||||
SplashContainerComponent,
|
||||
RegisterComponent,
|
||||
AddEmailToAccountMigrationModalComponent,
|
||||
ConfirmMigrationEmailComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RegistrationRoutingModule,
|
||||
NgbTooltipModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class RegistrationModule { }
|
||||
27
UI/Web/src/app/registration/registration.router.module.ts
Normal file
27
UI/Web/src/app/registration/registration.router.module.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { ConfirmEmailComponent } from './confirm-email/confirm-email.component';
|
||||
import { ConfirmMigrationEmailComponent } from './confirm-migration-email/confirm-migration-email.component';
|
||||
import { RegisterComponent } from './register/register.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'confirm-email',
|
||||
component: ConfirmEmailComponent,
|
||||
},
|
||||
{
|
||||
path: 'confirm-migration-email',
|
||||
component: ConfirmMigrationEmailComponent,
|
||||
},
|
||||
{
|
||||
path: 'register',
|
||||
component: RegisterComponent,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes), ],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class RegistrationRoutingModule { }
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<div class="mx-auto login">
|
||||
|
||||
<div class="row row-cols-4 row-cols-md-4 row-cols-sm-2 row-cols-xs-2">
|
||||
<div class="col align-self-center card p-3 m-3" style="max-width: 12rem;">
|
||||
<span>
|
||||
<div class="logo-container">
|
||||
<h3 class="card-title text-center">
|
||||
<ng-content select="[title]"></ng-content>
|
||||
</h3>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div class="card-text">
|
||||
<ng-content select="[body]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
@use "../../../theme/colors";
|
||||
|
||||
.login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: -61px; // To offset the navbar
|
||||
height: calc(100vh);
|
||||
min-height: 289px;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
background-image: url('../../../assets/images/login-bg.jpg');
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
.logo {
|
||||
display:inline-block;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: colors.$primary-color;
|
||||
color: #fff;
|
||||
min-width: 300px;
|
||||
|
||||
&:focus {
|
||||
border: 2px solid white;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.card-title {
|
||||
font-family: 'Spartan', sans-serif;
|
||||
font-weight: bold;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
font-family: "EBGaramond", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.alt {
|
||||
background-color: #424c72;
|
||||
border-color: #444f75;
|
||||
}
|
||||
|
||||
.alt:hover {
|
||||
background-color: #3b4466;
|
||||
}
|
||||
|
||||
.alt:focus {
|
||||
background-color: #343c59;
|
||||
box-shadow: 0 0 0 0.2rem rgb(68 79 117 / 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
display: inline-block;
|
||||
color: #343c59;
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #fff !important;
|
||||
color: black;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-splash-container',
|
||||
templateUrl: './splash-container.component.html',
|
||||
styleUrls: ['./splash-container.component.scss']
|
||||
})
|
||||
export class SplashContainerComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" [innerHtml]="config.content | safeHtml">
|
||||
<div class="modal-body" style="overflow-x: auto" [innerHtml]="config.content | safeHtml">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div *ngFor="let btn of config.buttons">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { ReactiveFormsModule } from '@angular/forms';
|
|||
import { NgbCollapseModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ConfirmDialogComponent } from './confirm-dialog/confirm-dialog.component';
|
||||
import { SafeHtmlPipe } from './safe-html.pipe';
|
||||
import { RegisterMemberComponent } from '../register-member/register-member.component';
|
||||
import { ReadMoreComponent } from './read-more/read-more.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DrawerComponent } from './drawer/drawer.component';
|
||||
|
|
@ -21,7 +20,6 @@ import { BadgeExpanderComponent } from './badge-expander/badge-expander.componen
|
|||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
RegisterMemberComponent,
|
||||
ConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
ReadMoreComponent,
|
||||
|
|
@ -45,7 +43,6 @@ import { BadgeExpanderComponent } from './badge-expander/badge-expander.componen
|
|||
NgCircleProgressModule.forRoot(),
|
||||
],
|
||||
exports: [
|
||||
RegisterMemberComponent,
|
||||
SafeHtmlPipe,
|
||||
SentenceCasePipe,
|
||||
ReadMoreComponent,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,6 @@
|
|||
<div class="mx-auto login">
|
||||
|
||||
<ng-container *ngIf="isLoaded">
|
||||
<div class="display: inline-block" *ngIf="firstTimeFlow">
|
||||
<h3 class="card-title text-center">Create an Admin Account</h3>
|
||||
<div class="card p-3">
|
||||
<p>Please create an admin account for yourself to start your reading journey.</p>
|
||||
<app-register-member (created)="onAdminCreated($event)" [firstTimeFlow]="firstTimeFlow"></app-register-member>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="loginForm" (ngSubmit)="login()" novalidate class="needs-validation" *ngIf="!firstTimeFlow">
|
||||
<div class="row row-cols-4 row-cols-md-4 row-cols-sm-2 row-cols-xs-2">
|
||||
<ng-container *ngFor="let member of memberNames">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { first, take } from 'rxjs/operators';
|
||||
import { SettingsService } from '../admin/settings.service';
|
||||
import { AddEmailToAccountMigrationModalComponent } from '../registration/add-email-to-account-migration-modal/add-email-to-account-migration-modal.component';
|
||||
import { User } from '../_models/user';
|
||||
import { AccountService } from '../_services/account.service';
|
||||
import { MemberService } from '../_services/member.service';
|
||||
|
|
@ -35,7 +37,7 @@ export class UserLoginComponent implements OnInit {
|
|||
isLoaded: boolean = false;
|
||||
|
||||
constructor(private accountService: AccountService, private router: Router, private memberService: MemberService,
|
||||
private toastr: ToastrService, private navService: NavService, private settingsService: SettingsService) { }
|
||||
private toastr: ToastrService, private navService: NavService, private settingsService: SettingsService, private modalService: NgbModal) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.navService.showNavBar();
|
||||
|
|
@ -62,6 +64,12 @@ export class UserLoginComponent implements OnInit {
|
|||
} else {
|
||||
this.memberService.adminExists().pipe(take(1)).subscribe(adminExists => {
|
||||
this.firstTimeFlow = !adminExists;
|
||||
|
||||
if (this.firstTimeFlow) {
|
||||
this.router.navigateByUrl('registration/register');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupAuthenticatedLoginFlow();
|
||||
this.isLoaded = true;
|
||||
});
|
||||
|
|
@ -95,7 +103,7 @@ export class UserLoginComponent implements OnInit {
|
|||
}
|
||||
|
||||
login() {
|
||||
this.model = {username: this.loginForm.get('username')?.value, password: this.loginForm.get('password')?.value};
|
||||
this.model = this.loginForm.getRawValue();
|
||||
this.accountService.login(this.model).subscribe(() => {
|
||||
this.loginForm.reset();
|
||||
this.navService.showNavBar();
|
||||
|
|
@ -109,9 +117,19 @@ export class UserLoginComponent implements OnInit {
|
|||
this.router.navigateByUrl('/library');
|
||||
}
|
||||
}, err => {
|
||||
this.toastr.error(err.error);
|
||||
if (err.error === 'You are missing an email on your account. Please wait while we migrate your account.') {
|
||||
// TODO: Implement this flow
|
||||
const modalRef = this.modalService.open(AddEmailToAccountMigrationModalComponent, { scrollable: true, size: 'md' });
|
||||
modalRef.componentInstance.username = this.model.username;
|
||||
modalRef.closed.pipe(take(1)).subscribe(() => {
|
||||
|
||||
});
|
||||
} else {
|
||||
this.toastr.error(err.error);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Move this into account service so it always happens
|
||||
this.accountService.currentUser$
|
||||
.pipe(first(x => (x !== null && x !== undefined && typeof x !== 'undefined')))
|
||||
.subscribe(currentUser => {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
color: $dark-text-color;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
a:not(.dark-exempt), .btn-link {
|
||||
color: $dark-hover-color;
|
||||
> i {
|
||||
color: $dark-hover-color !important;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,21 @@ body {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn.btn-secondary.alt {
|
||||
background-color: #424c72;
|
||||
border-color: #444f75;
|
||||
|
||||
&:hover {
|
||||
background-color: #3b4466;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: #343c59;
|
||||
box-shadow: 0 0 0 0.2rem rgb(68 79 117 / 50%);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app-root {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue