Skip to content

Commit

Permalink
Add sharing capabilities between users
Browse files Browse the repository at this point in the history
  • Loading branch information
kawazoe committed Jul 19, 2020
1 parent 679bd5e commit 37079d4
Show file tree
Hide file tree
Showing 19 changed files with 405 additions and 23 deletions.
17 changes: 17 additions & 0 deletions Westmoor.DowntimePlanner/ClientApp/src/app/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,19 @@ export interface UpdateApiKeyRequest {
sharedWith: string[];
}

export interface UserResponse {
userId: string;
email: string;
picture: string;
name: string;
userMetadata: UserMetadataResponse;
}

export interface UserMetadataResponse {
ownershipId: string;
campaigns: string[];
}

@Injectable({
providedIn: 'root'
})
Expand Down Expand Up @@ -227,4 +240,8 @@ export class ApiService {
public deleteApiKey(id: string): Observable<void> {
return this.http.delete<void>(`${this.endpoint}/apiKey/${id}`);
}

public searchUsersByEmail(email: string): Observable<UserResponse[]> {
return this.http.get<UserResponse[]>(`${this.endpoint}/user/${email}`);
}
}
39 changes: 30 additions & 9 deletions Westmoor.DowntimePlanner/ClientApp/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

import { RouterModule } from '@angular/router';
Expand All @@ -12,6 +12,7 @@ import { ButtonsModule } from 'ngx-bootstrap/buttons';
import { CollapseModule } from 'ngx-bootstrap/collapse';
import { ModalModule } from 'ngx-bootstrap/modal';
import { ProgressbarModule } from 'ngx-bootstrap/progressbar';
import { TypeaheadModule } from 'ngx-bootstrap/typeahead';

import { AuthHttpInterceptorService } from './auth-http-interceptor.service';
import { SignOutGuard } from './sign-out-guard.service';
Expand Down Expand Up @@ -71,24 +72,44 @@ import { FilterPipe } from './Pipes/filter.pipe';
FilterPipe
],
imports: [
BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }),
BrowserModule.withServerTransition({appId: 'ng-cli-universal'}),
BrowserAnimationsModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule,
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'activities', component: ActivitiesComponent, pathMatch: 'full', canActivate: [AuthGuard], data: { role: 'Admin' } },
{ path: 'characters', component: CharactersComponent, pathMatch: 'full', canActivate: [AuthGuard], data: { role: 'Admin' } },
{ path: 'api-keys', component: ApiKeysComponent, pathMatch: 'full', canActivate: [AuthGuard], data: { role: 'Admin' } },
{ path: 'signout', component: HomeComponent, pathMatch: 'full', canActivate: [SignOutGuard] },
{ path: 'oidc-callback', component: HomeComponent, pathMatch: 'full', canActivate: [OidcCallbackGuard] },
{path: '', component: HomeComponent, pathMatch: 'full'},
{
path: 'activities',
component: ActivitiesComponent,
pathMatch: 'full',
canActivate: [AuthGuard],
data: {role: 'Admin'}
},
{
path: 'characters',
component: CharactersComponent,
pathMatch: 'full',
canActivate: [AuthGuard],
data: {role: 'Admin'}
},
{
path: 'api-keys',
component: ApiKeysComponent,
pathMatch: 'full',
canActivate: [AuthGuard],
data: {role: 'Admin'}
},
{path: 'signout', component: HomeComponent, pathMatch: 'full', canActivate: [SignOutGuard]},
{path: 'oidc-callback', component: HomeComponent, pathMatch: 'full', canActivate: [OidcCallbackGuard]},
]),
AlertModule.forRoot(),
BsDropdownModule.forRoot(),
ButtonsModule.forRoot(),
CollapseModule.forRoot(),
ModalModule.forRoot(),
ProgressbarModule.forRoot()
ProgressbarModule.forRoot(),
TypeaheadModule.forRoot()
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptorService, multi: true }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.typeahead-item img {
width: 3rem;
height: 3rem;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
<div class="form-group">
<label for="shareWithId">User</label>
<div class="input-group">
<input type="text" class="form-control" id="shareWithId" [value]="sharedWithId" (change)="sharedWithId = $event.target.value">
<div class="input-group-append">
<button type="button" class="btn btn-success mb-2" (click)="share(sharedWithId)">Share...</button>
<ng-template #userResult let-item="item">
<div class="typeahead-item">
<img class="rounded-circle" [src]="item.picture || '/assets/default-profile.svg'">
<span class="ml-4">{{item.name}} - {{item.email}}</span>
</div>
</div>
</ng-template>

<input type="text"
class="form-control"
id="shareWith"
[(ngModel)]="search"
[typeahead]="usersByEmail$"
[typeaheadAsync]="true"
[typeaheadItemTemplate]="userResult"
(typeaheadOnSelect)="share($event)"
typeaheadOptionField="email"
placeholder="Add by email...">
</div>
<table class="table table-striped">
<thead>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Component, Input } from '@angular/core';
import { FormArray, FormControl } from '@angular/forms';
import { Observable, Observer, of } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
import { ApiService, UserResponse } from '../api.service';
import { TypeaheadMatch } from 'ngx-bootstrap/typeahead';

@Component({
selector: 'app-ownership',
Expand All @@ -9,20 +13,43 @@ import { FormArray, FormControl } from '@angular/forms';
export class OwnershipComponent {
@Input() public sharedWith: FormArray;

public sharedWithId = '';
public search: string;
usersByEmail$ = new Observable((o: Observer<string>) => {
o.next(this.search);
})
.pipe(
debounceTime(500),
switchMap(query => {
if (!query || query.length < 2) {
return of([]);
}

public share(id: string) {
if (!id) {
return this.api.searchUsersByEmail(query);
})
);

constructor(
private api: ApiService
) {
}

public share(match: TypeaheadMatch) {
const user = match.item as UserResponse;

this.search = '';

const ownershipId = user.userMetadata.ownershipId;
if (!ownershipId) {
return;
}

const index = this.sharedWith.controls.findIndex(c => c.value === id);
const index = this.sharedWith.controls.findIndex(c => c.value === ownershipId);

if (index !== -1) {
return;
}

this.sharedWith.push(new FormControl(id));
this.sharedWith.push(new FormControl(ownershipId));
}

public delete(id: string) {
Expand Down
25 changes: 25 additions & 0 deletions Westmoor.DowntimePlanner/Controllers/UserController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Westmoor.DowntimePlanner.Responses;
using Westmoor.DowntimePlanner.Services;

namespace Westmoor.DowntimePlanner.Controllers
{
[ApiController]
[Route("api/v1/[controller]")]
[Authorize(Policy = Policies.ReadUsers)]
public class UserController : ControllerBase
{
private readonly IUserService _service;

public UserController(IUserService service)
{
_service = service;
}

[HttpGet("{email}")]
public async Task<UserResponse[]> SearchByEmailAsync(string email) =>
await _service.SearchByEmailAsync(email);
}
}
13 changes: 13 additions & 0 deletions Westmoor.DowntimePlanner/Entities/OauthTokenEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace Westmoor.DowntimePlanner.Entities
{
public class OauthTokenEntity
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }

[JsonPropertyName("token_type")]
public string TokenType { get; set; }
}
}
22 changes: 22 additions & 0 deletions Westmoor.DowntimePlanner/Entities/UserEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;

namespace Westmoor.DowntimePlanner.Entities
{
public class UserEntity
{
[JsonPropertyName("user_id")]
public string UserId { get; set; }

[JsonPropertyName("email")]
public string Email { get; set; }

[JsonPropertyName("picture")]
public string Picture { get; set; }

[JsonPropertyName("name")]
public string Name { get; set; }

[JsonPropertyName("user_metadata")]
public UserMetadataEntity UserMetadata { get; set; }
}
}
13 changes: 13 additions & 0 deletions Westmoor.DowntimePlanner/Entities/UserMetadataEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;

namespace Westmoor.DowntimePlanner.Entities
{
public class UserMetadataEntity
{
[JsonPropertyName("ownership_id")]
public string OwnershipId { get; set; }

[JsonPropertyName("campaigns")]
public string[] Campaigns { get; set; }
}
}
2 changes: 2 additions & 0 deletions Westmoor.DowntimePlanner/Policies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public static class Policies
public const string ReadActivities = "read:activities";
public const string ReadCharacters = "read:characters";
public const string ReadDowntimes = "read:downtimes";
public const string ReadUsers = "read:users";
public const string WriteActivities = "write:activities";
public const string WriteApiKeys = "write:apikeys";
public const string WriteCharacters = "write:characters";
Expand All @@ -14,6 +15,7 @@ public static class Policies
ReadActivities,
ReadCharacters,
ReadDowntimes,
ReadUsers,
WriteActivities,
WriteApiKeys,
WriteCharacters,
Expand Down
Loading

0 comments on commit 37079d4

Please sign in to comment.