Skip to content

Commit

Permalink
Add guards for actions on notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
sondreb committed Aug 25, 2024
1 parent 36c806c commit 9a4dcf6
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 5 deletions.
12 changes: 10 additions & 2 deletions app/src/app/connection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ export class ConnectionService {

console.log('Connection created:', status, record);

return {
this.connections.update((list) => [...list, entry]);

const entry = {
record,
data: eventData,
id: record!.id,
} as ConnectionEntry;

return entry;
}

/** Loads the connections and blocks */
Expand Down Expand Up @@ -121,11 +125,15 @@ export class ConnectionService {

console.log('Block created:', status, record);

return {
const entry = {
record,
data,
id: record!.id,
} as ConnectionBlockEntry;

this.blocks.update((list) => [...list, entry]);

return entry;
}

async loadRequests() {
Expand Down
16 changes: 16 additions & 0 deletions app/src/app/friend.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { FriendService } from './friend.service';

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

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

it('should be created', () => {
expect(service).toBeTruthy();
});
});
256 changes: 256 additions & 0 deletions app/src/app/friend.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { inject, Injectable, signal } from '@angular/core';
import { IdentityService } from './identity.service';
import { AppService } from './app.service';
import { protocolDefinition as messageDefinition } from '../protocols/message';
import { Record } from '@web5/api';
import { VerifiableCredential } from '@web5/credentials';
import { credential, message } from '../protocols';

export interface Entry {
record: Record;
data: any;
id: string;
direction: 'in' | 'out' | any;
}

@Injectable({
providedIn: 'root',
})
export class FriendService {
requests = signal<Entry[]>([]);

friends = signal<any[]>([]);

identity = inject(IdentityService);

app = inject(AppService);

constructor() {}

async accept(entry: Entry) {
// TOOD: We should obviously verify the incoming VC is correct, that it belongs to the
// user that sent it, etc. But for now, we'll just accept it. If we don't validate, anyone
// could send us a VC and we'd accept it, opening up a friend connection that is incorrect.
// This validation should be done before even showing the request to the user, and a delete
// request should be sent to the sender if the validation fails.
//
// We will perform additional verification here, to avoid accepting a request that is invalid.

const signedVcJwt = entry.data.vc;

console.log('signedVcJwt:', signedVcJwt);

if (!signedVcJwt) {
return;
}

try {
await VerifiableCredential.verify({ vcJwt: signedVcJwt });
} catch (error) {
console.error('Error verifying VC:', error);
return;
}

const vc = VerifiableCredential.parseJwt({ vcJwt: signedVcJwt });
const targetDid = vc.issuer;

// If the VC issuer is different than data record author, then reject the request.
if (vc.issuer != entry.record.author) {
console.error('VC issuer is different than data record author');
return;
}

console.log('Friend request validated');

const twoWayVC = await VerifiableCredential.create({
type: credential.friendship,
issuer: this.identity.did,
subject: targetDid,
data: {
vc: signedVcJwt,
},
});

console.log('TWO WAY VC:', twoWayVC);

const bearerDid = await this.identity.activeAgent().identity.get({ didUri: this.identity.did });
const vc_jwt = await twoWayVC.sign({ did: bearerDid!.did });
console.log('TWO WAY VC JWT:', vc_jwt);

// Persist the two-way VC, these are the only ones that we store for safe-keeping, not the one-way.
const { record } = await this.identity.web5.dwn.records.create({
data: vc_jwt,
message: {
schema: credential.friendship,
dataFormat: credential.format,
published: false,
},
});
console.log('TWO WAY VC RECORD:', record);

const { status } = await record!.send(this.identity.did);
console.log('Record sent:', status, record);

// Next step is to send the VC to the sender of the request, so they can also have a two-way VC.
// VCs can be sent to anyone, even if they are not in the user's DWN. This is a way to establish
// various connections. VCs are automatically or manually accepted by users.
const { status: requestCreateStatus, record: messageRecord } = await this.identity.web5.dwn.records.create({
data: { vc: vc_jwt },
store: false, // We don't need to store a copy of this locally.
message: {
recipient: targetDid,
protocol: messageDefinition.protocol,
protocolPath: 'credential',
schema: messageDefinition.types.credential.schema,
dataFormat: messageDefinition.types.credential.dataFormats[0],
},
});

console.log('Request create status:', requestCreateStatus);

const { status: requestStatus } = await messageRecord!.send(targetDid);

if (requestStatus.code !== 202) {
this.app.openSnackBar(`Friend request failed.Code: ${requestStatus.code}, Details: ${requestStatus.detail}.`);
} else {
this.app.openSnackBar('Friend request accepted');

// Remove the accepted entry from the requests list
await this.reject(entry);
}
}

async reject(entry: Entry) {
console.log('Rejecting request:', entry);

// If the recipinent is the current user, then use the author as the target DID.
// Very important to read this BEFORE running local delete, as that mutates the record.
const targetDid = entry.record.recipient == this.identity.did ? entry.record.author : entry.record.recipient;

console.log('Target DID:', targetDid);
console.log('this.identity.did:', this.identity.did);
console.log('entry.record.recipient:', entry.record.recipient);
console.log('entry.record.author:', entry.record.author);

// delete the request from the local DWN
const { status: deleteStatus } = await entry.record.delete();

// send the delete request to the remote DWN
const { status: deleteSendStatus } = await entry.record.send(targetDid);

console.log('Delete status:', deleteStatus);
console.log('deleteSendStatus:', deleteSendStatus);

// Remove the deleted entry from the requests list
this.requests.update((requests) => requests.filter((request) => request !== entry));
}

async processFriends() {
// TODO: Processing incoming accepted friend requests should happen in a backgrond task, not here.

// Get all incoming VC records in the message protocol.
var { records: vcRecords } = await this.identity.web5.dwn.records.query({
message: {
filter: {
protocol: message.uri,
schema: messageDefinition.types.credential.schema,
dataFormat: messageDefinition.types.credential.dataFormats[0],
},
},
});

console.log('VC Records:', vcRecords);

// Automatically accept all incoming friend requests VCs, but validate that the
// inner VC is correct.

for (const record of vcRecords!) {
console.log('Record:', record);

const json = await record.data.json();
console.log('JSON:', json);

try {
await VerifiableCredential.verify({ vcJwt: json.vc });
} catch (error) {
console.error('Error verifying VC:', error);
console.log('THIS REQUEST SHOULD BE DELETED FROM DWN', record.id);
}

const vc = VerifiableCredential.parseJwt({ vcJwt: json.vc });

console.log('PARSED INVCOMING VC:', vc);
console.log('vc.issuer === this.identity.did:', vc.issuer === this.identity.did);

// Validate that the inner VC is ours, if OK, we can go ahead and persist the VC.
// if (vc.issuer === this.identity.did) {
// Persist the two-way VC, these are the only ones that we store for safe-keeping, not the one-way.
const { record: record2 } = await this.identity.web5.dwn.records.create({
data: json.vc,
message: {
schema: credential.friendship,
dataFormat: credential.format,
published: false,
},
});
console.log('TWO WAY VC RECORD:', record2);

const { status } = await record2!.send(this.identity.did);
console.log('Record sent:', status, record2);

// Delete the incoming VC record, as it has been processed.
await record?.delete();
// }
}
}

async loadFriends() {
var { records } = await this.identity.web5.dwn.records.query({
message: {
filter: {
schema: credential.friendship,
dataFormat: credential.format,
},
},
});

console.log('Friend VCs:');
console.log(records);

let json = {};
let recordEntry = null;

this.friends.set([]);

if (records) {
// Loop through returned records and print text from each
for (const record of records!) {
let data = await record.data.text();
let vc = VerifiableCredential.parseJwt({ vcJwt: data });

let did = vc.issuer;

// If the outher issuer is us, get the inner one.
if (vc.issuer == this.identity.did) {
const subject = vc.vcDataModel.credentialSubject as any;
let vcInner = VerifiableCredential.parseJwt({ vcJwt: subject.vc });
did = vcInner.issuer;
}

let json: any = { record: record, data: { did } };

// if (record.author == this.identity.did) {
// json.direction = 'out';
// }

this.friends.update((requests) => [...requests, json]);

console.log('All friends:', this.friends());

// recordEntry = record;
// let recordJson = await record.data.json();
// json = { ...recordJson, id: record.dataCid, did: record.author, created: record.dateCreated };
}
}
}
}
1 change: 1 addition & 0 deletions app/src/app/notification.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface NotificationEvent {
record: Record;
id: string;
data: any;
loading?: boolean;
}

@Injectable({
Expand Down
6 changes: 3 additions & 3 deletions app/src/app/notifications/notifications.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ <h1>Notifications</h1>
}
</mat-card-content>
<mat-card-actions>
<button mat-flat-button (click)="accept(entry)">ACCEPT</button>
<button mat-button (click)="deleteNotification(entry)">DELETE</button>
<button mat-button (click)="block(entry)">BLOCK</button>
<button mat-flat-button (click)="accept(entry)" [disabled]="entry.loading">ACCEPT</button>
<button mat-button (click)="deleteNotification(entry)" [disabled]="entry.loading">DELETE</button>
<button mat-button (click)="block(entry)" [disabled]="entry.loading">BLOCK</button>
</mat-card-actions>
</mat-card>
}
Expand Down
18 changes: 18 additions & 0 deletions app/src/app/notifications/notifications.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export class NotificationsComponent {
}

async accept(entry: NotificationEvent) {
if (entry.loading) {
return;
}

entry.loading = true;

console.log('Accepting connection request');

if (entry.data.app === 'Friends') {
Expand Down Expand Up @@ -86,6 +92,12 @@ export class NotificationsComponent {
}

async deleteNotification(entry: NotificationEvent) {
if (entry.loading) {
return;
}

entry.loading = true;

const did = entry.record.author;

// Find all connection requests from this user and delete them.
Expand All @@ -106,6 +118,12 @@ export class NotificationsComponent {
}

async block(entry: NotificationEvent) {
if (entry.loading) {
return;
}

entry.loading = true;

console.log('Blocking user', entry);

const did = entry.record.author;
Expand Down

0 comments on commit 9a4dcf6

Please sign in to comment.