Switch to sending Channel info to apps
Instead of account info. Stubbed out the hub work. Haven't made the example app accommodate it. Plenty left to do, but this lays down the order of things.
This commit is contained in:
parent
d0583e540a
commit
1c7854b9ec
16 changed files with 350 additions and 208 deletions
|
@ -1,70 +1,96 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
import {CryptoService} from './crypto.service';
|
||||
import {GlobalVarsService} from './global-vars.service';
|
||||
import {AccessLevel, PrivateAccountInfo, PublicChannelInfo} from '../types/identity';
|
||||
import {HubService} from './hub.service';
|
||||
import {AccessLevel, ActionType, PrivateAccountInfo, PublicChannelInfo} from '../types/identity';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AccountService {
|
||||
private static levelsStorageKey = 'levels';
|
||||
private static walletStorageKey = 'wallet';
|
||||
private static channelsStorageKey = 'channels';
|
||||
private static accessStorageKey = 'access';
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private hubService: HubService,
|
||||
) { }
|
||||
|
||||
// Public Getters
|
||||
|
||||
// TODO - As of this writing, we want to share channel claim ids with the
|
||||
// account on login, and spending addresses on request (probably with
|
||||
// explicit permission)
|
||||
getChannels(): {[key: string]: PublicChannelInfo} {
|
||||
// TODO - will want for accessLevel stuff
|
||||
// const hostname = this.globalVars.hostname;
|
||||
|
||||
const privateAccounts = this.getWalletAccounts();
|
||||
const channels: {[key: string]: PublicChannelInfo} = {};
|
||||
|
||||
for (const name of Object.keys(privateAccounts)) {
|
||||
const privateAccount = privateAccounts[name];
|
||||
for (const channelPubKeyAddress of Object.keys(privateAccount.certificates)) {
|
||||
// TODO - For LBRY's purposes, not only will we want per-channel access
|
||||
// levels, we'll want per channel per hostname per action access levels.
|
||||
|
||||
// TODO - finish when we have accessLevel stuff
|
||||
/*
|
||||
const accessLevel = this.getAccessLevel(name, hostname);
|
||||
if (accessLevel === AccessLevel.None) {
|
||||
continue;
|
||||
|
||||
What we're keeping in local storage. TODO - make these into data structs.
|
||||
|
||||
// The wallet, taken from wallet Sync. Perhaps even with local changes to be
|
||||
// pushed back to sync.
|
||||
localStorage["wallet"] = {
|
||||
// We may later want to add signature, sync metadata, etc. Or maybe we want
|
||||
// the strict string representation that came out of sync.
|
||||
walletStoreVersion: 0,
|
||||
|
||||
wallet: "...",
|
||||
}
|
||||
|
||||
// TODO - Implement the hmac properly
|
||||
// TODO - why do we even have hmac if everything's in local storage anyway?
|
||||
const accessLevelHmac = this.cryptoService.accessLevelHmac(accessLevel, privateAccount.seed);
|
||||
// Access information. For each hostname, what channel is logged in, and what
|
||||
// level of permission for various actions did the user give?
|
||||
localStorage["access"] = {
|
||||
// There can be multiple hostnames
|
||||
"<hostnames>": { // TODO - type Hostname
|
||||
"currentChannel": "<channel-claim-id>", // TODO - type ChannelClaimID
|
||||
"levels": {
|
||||
// There can be multiple channels
|
||||
"<channel-claim-ids>": {
|
||||
// There can be multiple action types
|
||||
"<action-types>": "<level>",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localStorage["channels"] = {
|
||||
// There can be multiple channels
|
||||
"<channel-claim-ids>": {
|
||||
"claimId": "<channel-claim-id>",
|
||||
"handle": "<channel-handle>",
|
||||
"pubKeyAddress": "<channel-pub-key-address>",
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
channels[channelPubKeyAddress] = {
|
||||
pubKeyAddress: channelPubKeyAddress,
|
||||
network: privateAccount.ledger,
|
||||
|
||||
// TODO - fill in when we have accessLevel stuff
|
||||
accessLevel: 0,
|
||||
accessLevelHmac: "",
|
||||
};
|
||||
}
|
||||
private hasWallet(): boolean {
|
||||
return !!localStorage.getItem(AccountService.walletStorageKey);
|
||||
}
|
||||
|
||||
return channels;
|
||||
// TODO define a wallet type, and/or use a type defined by json-schema
|
||||
public getWallet(): {accounts: [PrivateAccountInfo]} | null {
|
||||
const walletStoreStr = localStorage.getItem(AccountService.walletStorageKey);
|
||||
const walletStore = JSON.parse(walletStoreStr || 'null')
|
||||
if (walletStore !== null) {
|
||||
if (walletStore.walletStoreVersion === 0) {
|
||||
return walletStore.wallet
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO - Need to confirm that this works I think
|
||||
public getWalletAccounts(): {[key: string]: PrivateAccountInfo} {
|
||||
const wallet = this.cryptoService.getWallet(this.globalVars.hostname)
|
||||
private putWallet(wallet: object | null) {
|
||||
const walletStore = {
|
||||
// We may later want to add signature, sync metadata, etc. Or maybe we
|
||||
// want the strict string representation that came out of sync. This is
|
||||
// not the version of the wallet's internal structure; that has its own
|
||||
// version key
|
||||
walletStoreVersion: 0,
|
||||
|
||||
wallet
|
||||
}
|
||||
localStorage.setItem(AccountService.walletStorageKey, JSON.stringify(walletStore));
|
||||
}
|
||||
|
||||
public getAccounts(): PrivateAccountInfo[] {
|
||||
const wallet = this.getWallet()
|
||||
if (wallet === null) {
|
||||
return {}
|
||||
return []
|
||||
}
|
||||
const filteredAccounts: {[key: string]: PrivateAccountInfo} = {};
|
||||
const filteredAccounts: PrivateAccountInfo[] = [];
|
||||
|
||||
for (const account of wallet.accounts) {
|
||||
// Only include accounts from the current network
|
||||
|
@ -72,15 +98,74 @@ export class AccountService {
|
|||
continue;
|
||||
}
|
||||
|
||||
filteredAccounts[account.name] = account;
|
||||
filteredAccounts.push(account);
|
||||
}
|
||||
return filteredAccounts
|
||||
}
|
||||
|
||||
getAccessLevel(accountName: string, hostname: string): AccessLevel {
|
||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
||||
const hostMapping = levels[hostname] || {};
|
||||
const accessLevel = hostMapping[accountName];
|
||||
private clearChannels() {
|
||||
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
|
||||
}
|
||||
|
||||
public updateChannels() {
|
||||
let xPubs: string[] = this.getAccounts().map(account => account.public_key)
|
||||
let channels: {[key: string]: PublicChannelInfo} = {};
|
||||
for (const hubChannel of this.hubService.getChannels(xPubs)) {
|
||||
channels[hubChannel.claimId] = {
|
||||
claimId: hubChannel.claimId,
|
||||
handle: hubChannel.handle,
|
||||
pubKeyAddress: hubChannel.pubKeyAddress,
|
||||
// TODO -- more fields?
|
||||
}
|
||||
}
|
||||
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(channels));
|
||||
}
|
||||
|
||||
public hasChannels() {
|
||||
return !!localStorage.getItem(AccountService.channelsStorageKey);
|
||||
}
|
||||
|
||||
public getChannels() {
|
||||
return JSON.parse(localStorage.getItem(AccountService.channelsStorageKey) || '{}');
|
||||
}
|
||||
|
||||
private clearAccess() {
|
||||
localStorage.setItem(AccountService.channelsStorageKey, JSON.stringify(null));
|
||||
}
|
||||
|
||||
private initAccess() {
|
||||
// no currentChannel or access level for any action on any hostname
|
||||
localStorage.setItem(AccountService.accessStorageKey, '{}');
|
||||
}
|
||||
|
||||
public hasAccess() {
|
||||
return !!localStorage.getItem(AccountService.accessStorageKey);
|
||||
}
|
||||
|
||||
public getActiveChannel(hostname: string): PublicChannelInfo | null {
|
||||
// TODO - and actually, this maybe only needs to happen on startup. could save in a local variable.
|
||||
const channels = this.getChannels()
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
|
||||
if (access[hostname]) {
|
||||
const activeChannelClaimId = access[hostname].currentChannel
|
||||
return channels[activeChannelClaimId]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
public getActiveChannelAccessLevel(hostname: string, action: ActionType): AccessLevel {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
|
||||
if (!access[hostname]) {
|
||||
return AccessLevel.None
|
||||
}
|
||||
const activeChannelClaimId = access[hostname].currentChannel
|
||||
if (!access[hostname].levels[activeChannelClaimId]) {
|
||||
return AccessLevel.None
|
||||
}
|
||||
|
||||
const accessLevel = access[hostname].levels[activeChannelClaimId][action]
|
||||
|
||||
if (Object.values(AccessLevel).includes(accessLevel)) {
|
||||
return accessLevel;
|
||||
|
@ -89,27 +174,63 @@ export class AccountService {
|
|||
}
|
||||
}
|
||||
|
||||
// Public Modifiers
|
||||
|
||||
setAccessLevel(accountName: string, hostname: string, accessLevel: AccessLevel): void {
|
||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
||||
|
||||
levels[hostname] ||= {};
|
||||
levels[hostname][accountName] = accessLevel;
|
||||
|
||||
localStorage.setItem(AccountService.levelsStorageKey, JSON.stringify(levels));
|
||||
public setAccessLevel(hostname: string, channelClaimId: string, action: ActionType, level: AccessLevel) {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
if (!(hostname in access)) {
|
||||
access[hostname] = {levels: {}}
|
||||
}
|
||||
if (!(channelClaimId in access[hostname].levels)) {
|
||||
access[hostname].levels[channelClaimId] = {}
|
||||
}
|
||||
access[hostname].levels[channelClaimId][action] = level
|
||||
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||
}
|
||||
|
||||
// log out of hostname entirely by setting accesslevel to None
|
||||
resetAccessLevels(hostname: string): void {
|
||||
const levels = JSON.parse(localStorage.getItem(AccountService.levelsStorageKey) || '{}');
|
||||
|
||||
levels[hostname] ||= {};
|
||||
for (const accountName in levels[hostname]) {
|
||||
levels[hostname][accountName] = AccessLevel.None;
|
||||
public setAccessCurrentChannel(hostname: string, channelClaimId: string) {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
if (!(hostname in access)) {
|
||||
access[hostname] = {levels: {}}
|
||||
}
|
||||
access[hostname].currentChannel = channelClaimId
|
||||
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||
}
|
||||
|
||||
localStorage.setItem(AccountService.levelsStorageKey, JSON.stringify(levels));
|
||||
public walletLogout() {
|
||||
this.putWallet(null)
|
||||
this.clearAccess()
|
||||
this.clearChannels()
|
||||
}
|
||||
|
||||
public walletLogin(wallet: object | null) {
|
||||
// no ambiguity from half-completed actions
|
||||
// make sure we're fully logged out
|
||||
this.walletLogout()
|
||||
|
||||
this.putWallet(wallet)
|
||||
this.initAccess()
|
||||
this.updateChannels()
|
||||
}
|
||||
|
||||
// TODO - delete this if I don't end up using it
|
||||
public walletIsLoggedIn(): boolean {
|
||||
return (
|
||||
// All three should be set. It means the last login was complete, and no
|
||||
// logout was started since.
|
||||
this.hasAccess() && this.hasChannels() && this.hasWallet()
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Do we want to save per-hostname-per-channel-per-action access levels
|
||||
// between logins? Counterargument: users probably won't log in and out too
|
||||
// often.
|
||||
private appClearAccess(hostname: string): void {
|
||||
const access = JSON.parse(localStorage.getItem(AccountService.accessStorageKey) || '{}');
|
||||
delete access[hostname]
|
||||
localStorage.setItem(AccountService.accessStorageKey, JSON.stringify(access));
|
||||
}
|
||||
|
||||
public appLogout(hostname: string): void {
|
||||
this.appClearAccess(hostname)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import { EmbedComponent } from './embed/embed.component';
|
|||
import { HomeComponent } from './home/home.component';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {IdentityService} from './identity.service';
|
||||
import {CookieModule} from 'ngx-cookie';
|
||||
import { LogoutComponent } from './logout/logout.component';
|
||||
import { BannerComponent } from './banner/banner.component';
|
||||
import { SignUpComponent } from './sign-up/sign-up.component';
|
||||
|
@ -50,7 +49,6 @@ import { LogInWalletComponent } from './log-in-wallet/log-in-wallet.component'
|
|||
NgxIntlTelInputModule,
|
||||
MatFormFieldModule,
|
||||
MatTooltipModule,
|
||||
CookieModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
IdentityService,
|
||||
|
|
|
@ -67,6 +67,9 @@ export class ApproveComponent implements OnInit {
|
|||
this.publicKey = this.base58KeyCheck(this.transaction.publicKey);
|
||||
|
||||
this.generateTransactionDescription();
|
||||
/*
|
||||
TODO this.accountService.getActiveChannelAccessLevel
|
||||
*/
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -82,6 +85,10 @@ export class ApproveComponent implements OnInit {
|
|||
const signedTransactionHex = this.signingService.signTransaction(seedHex, this.transactionHex);
|
||||
this.finishFlow(signedTransactionHex);
|
||||
*/
|
||||
|
||||
/*
|
||||
TODO this.accountService.setAccessLevel if people want to keep allowing the action
|
||||
*/
|
||||
}
|
||||
|
||||
finishFlow(signedTransactionHex?: string): void {
|
||||
|
|
|
@ -3,8 +3,7 @@ import {HttpClient} from '@angular/common/http';
|
|||
import {Observable, of} from 'rxjs';
|
||||
import {catchError, map} from 'rxjs/operators';
|
||||
import {environment} from '../environments/environment';
|
||||
import {CryptoService} from './crypto.service';
|
||||
import {GlobalVarsService} from './global-vars.service';
|
||||
import {AccountService} from './account.service';
|
||||
|
||||
export class ProfileEntryResponse {
|
||||
Username: string | null = null;
|
||||
|
@ -25,8 +24,7 @@ export class BackendAPIService {
|
|||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private accountService: AccountService,
|
||||
) { }
|
||||
|
||||
post(path: string, body: any): Observable<any> {
|
||||
|
@ -80,7 +78,7 @@ export class BackendAPIService {
|
|||
): Observable<{bodyJson: string, signature: string} | null> {
|
||||
|
||||
// A stub for now
|
||||
const wallet : object | null = this.cryptoService.getWallet(this.globalVars.hostname);
|
||||
const wallet : object | null = this.accountService.getWallet();
|
||||
if (wallet === null) {
|
||||
return of(null)
|
||||
}
|
||||
|
|
|
@ -1,101 +1,32 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {ec as EC} from 'elliptic';
|
||||
import bs58check from 'bs58check';
|
||||
import {CookieService} from 'ngx-cookie';
|
||||
import {createHmac, createCipher, createDecipher, randomBytes} from 'crypto';
|
||||
import {AccessLevel, PrivateAccountInfo} from '../types/identity';
|
||||
import { GlobalVarsService } from './global-vars.service';
|
||||
import {AccessLevel} from '../types/identity';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CryptoService {
|
||||
|
||||
constructor(
|
||||
private cookieService: CookieService,
|
||||
private globalVars: GlobalVarsService
|
||||
) {}
|
||||
|
||||
// Safari only lets us store things in cookies
|
||||
mustUseStorageAccess(): boolean {
|
||||
// Webviews have full control over storage access
|
||||
if (this.globalVars.webview) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const supportsStorageAccess = typeof document.hasStorageAccess === 'function';
|
||||
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
|
||||
const isSafari = !isChrome && navigator.userAgent.indexOf('Safari') > -1;
|
||||
|
||||
// Firefox and Edge support the storage access API but do not enforce it.
|
||||
// For now, only use cookies if we support storage access and use Safari.
|
||||
const mustUseStorageAccess = supportsStorageAccess && isSafari;
|
||||
|
||||
return mustUseStorageAccess;
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
// 32 bytes = 256 bits is plenty of entropy for encryption
|
||||
newEncryptionKey(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
// TODO we won't need this soon right?
|
||||
seedHexEncryptionStorageKey(hostname: string): string {
|
||||
return `seed-hex-key-${hostname}`;
|
||||
}
|
||||
|
||||
// Alternate plan is to use this same system, but instead of encrypting the
|
||||
// seed and sending it back and forth, we do the wallet. It may be superior
|
||||
// because at most times the decrypted wallet is not accessible anywhere
|
||||
// without sending a message between the app and identity service. But we'd
|
||||
// have to trust that we'll never accidentally send the wallet to the app
|
||||
// unencrypted.
|
||||
walletStorageKey(hostname: string): string {
|
||||
return `wallet-key-${hostname}`;
|
||||
}
|
||||
|
||||
hasWallet(hostname: string): boolean {
|
||||
const storageKey = this.walletStorageKey(hostname);
|
||||
|
||||
if (this.mustUseStorageAccess()) {
|
||||
return !!this.cookieService.get(storageKey);
|
||||
} else {
|
||||
return !!localStorage.getItem(storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
hasSeedHexEncryptionKey(hostname: string): boolean {
|
||||
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
||||
|
||||
if (this.mustUseStorageAccess()) {
|
||||
return !!this.cookieService.get(storageKey);
|
||||
} else {
|
||||
return !!localStorage.getItem(storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO define a wallet type, and/or use a type defined by json-schema
|
||||
getWallet(hostname: string): {accounts: [PrivateAccountInfo]} | null {
|
||||
const storageKey = this.walletStorageKey(hostname);
|
||||
let walletStr
|
||||
if (this.mustUseStorageAccess()) {
|
||||
walletStr = this.cookieService.get(storageKey);
|
||||
} else {
|
||||
walletStr = localStorage.getItem(storageKey);
|
||||
}
|
||||
return JSON.parse(walletStr || 'null')
|
||||
}
|
||||
|
||||
putWallet(hostname: string, wallet: object | null) {
|
||||
const storageKey = this.walletStorageKey(hostname);
|
||||
|
||||
if (this.mustUseStorageAccess()) {
|
||||
this.cookieService.put(storageKey, JSON.stringify(wallet), {
|
||||
expires: new Date('2100/01/01 00:00:00'),
|
||||
});
|
||||
} else {
|
||||
localStorage.setItem(storageKey, JSON.stringify(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
// Place a seed encryption key in storage. If reset is set to true, the
|
||||
// previous key is overwritten, which is useful in logging out users.
|
||||
|
@ -103,21 +34,11 @@ export class CryptoService {
|
|||
const storageKey = this.seedHexEncryptionStorageKey(hostname);
|
||||
let encryptionKey;
|
||||
|
||||
if (this.mustUseStorageAccess()) {
|
||||
encryptionKey = this.cookieService.get(storageKey);
|
||||
if (!encryptionKey || reset) {
|
||||
encryptionKey = this.newEncryptionKey();
|
||||
this.cookieService.put(storageKey, encryptionKey, {
|
||||
expires: new Date('2100/01/01 00:00:00'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
encryptionKey = localStorage.getItem(storageKey) || '';
|
||||
if (!encryptionKey || reset) {
|
||||
encryptionKey = this.newEncryptionKey();
|
||||
localStorage.setItem(storageKey, encryptionKey);
|
||||
}
|
||||
}
|
||||
|
||||
// If the encryption key is unset or malformed we need to stop
|
||||
// everything to avoid returning unencrypted information.
|
||||
|
|
16
src/app/hub.service.spec.ts
Normal file
16
src/app/hub.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HubService } from './account.service';
|
||||
|
||||
describe('HubService', () => {
|
||||
let service: HubService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(HubService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
29
src/app/hub.service.ts
Normal file
29
src/app/hub.service.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HubService {
|
||||
|
||||
constructor(
|
||||
) { }
|
||||
|
||||
// Just for stubbing until we get the real thing
|
||||
private rndStr() {
|
||||
return (Math.random() + 1).toString(16).substring(2);
|
||||
}
|
||||
|
||||
// Obviously just a stub for the actual API
|
||||
public getChannels(xPubs: string[]): any {
|
||||
return [{
|
||||
claimId: this.rndStr(),
|
||||
handle: '@test-' + this.rndStr(),
|
||||
pubKeyAddress: this.rndStr(),
|
||||
}, {
|
||||
claimId: this.rndStr(),
|
||||
handle: '@test-' + this.rndStr(),
|
||||
pubKeyAddress: this.rndStr(),
|
||||
}]
|
||||
}
|
||||
|
||||
}
|
|
@ -4,7 +4,6 @@ import {v4 as uuid} from 'uuid';
|
|||
import {AccessLevel, PublicChannelInfo} from '../types/identity';
|
||||
import {CryptoService} from './crypto.service';
|
||||
import {GlobalVarsService} from './global-vars.service';
|
||||
import {CookieService} from 'ngx-cookie';
|
||||
import {SigningService} from './signing.service';
|
||||
import {HttpParams} from '@angular/common/http';
|
||||
|
||||
|
@ -24,12 +23,30 @@ export class IdentityService {
|
|||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private cookieService: CookieService,
|
||||
private signingService: SigningService,
|
||||
) {
|
||||
window.addEventListener('message', (event) => this.handleMessage(event));
|
||||
}
|
||||
|
||||
// Safari only lets us store things in cookies
|
||||
mustUseStorageAccess(): boolean {
|
||||
// Webviews have full control over storage access
|
||||
// TODO why do we trust the app to send this properly
|
||||
if (this.globalVars.webview) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const supportsStorageAccess = typeof document.hasStorageAccess === 'function';
|
||||
const isChrome = navigator.userAgent.indexOf('Chrome') > -1;
|
||||
const isSafari = !isChrome && navigator.userAgent.indexOf('Safari') > -1;
|
||||
|
||||
// Firefox and Edge support the storage access API but do not enforce it.
|
||||
// For now, only use cookies if we support storage access and use Safari.
|
||||
const mustUseStorageAccess = supportsStorageAccess && isSafari;
|
||||
|
||||
return mustUseStorageAccess;
|
||||
}
|
||||
|
||||
// Outgoing Messages
|
||||
|
||||
initialize(): Observable<any> {
|
||||
|
@ -41,8 +58,7 @@ export class IdentityService {
|
|||
}
|
||||
|
||||
login(payload: {
|
||||
channels: {[key: string]: PublicChannelInfo},
|
||||
accountNameAdded?: string, // Which channel id was just authorized. the app may allow the user to switch between them, but this determines which is on now.
|
||||
channel: PublicChannelInfo | null,
|
||||
signedUp?: boolean
|
||||
signedTransactionHex?: string,
|
||||
addresses?: string[],
|
||||
|
@ -118,7 +134,7 @@ export class IdentityService {
|
|||
private async handleInfo(event: MessageEvent): Promise<void> {
|
||||
// check storage access API
|
||||
let hasStorageAccess = true;
|
||||
if (this.cryptoService.mustUseStorageAccess()) {
|
||||
if (this.mustUseStorageAccess()) {
|
||||
hasStorageAccess = await document.hasStorageAccess();
|
||||
}
|
||||
|
||||
|
@ -130,15 +146,37 @@ export class IdentityService {
|
|||
hasLocalStorageAccess = false;
|
||||
}
|
||||
|
||||
// check for cookie access
|
||||
this.cookieService.put('deso-test-access', 'true');
|
||||
const hasCookieAccess = !!this.cookieService.get('deso-test-access');
|
||||
// TODO - Sort out the storage access issue:
|
||||
//
|
||||
// There was a part of the code that was defaulting to cookies if
|
||||
// this.mustUseStorageAccess() was true (only applies to Safari). I don't
|
||||
// want to do that because I'm afraid the data we'll be storing won't fit.
|
||||
// So, I'm going to consider browsers with this.mustUseStorageAccess() as
|
||||
// unsupported until we can get back and figure out why DeSo chose to go
|
||||
// with cookies for those cases.
|
||||
//
|
||||
// It looks like the issue is that Safari only supports saving in cookies
|
||||
// (per the comment above this.mustUseStorageAccess(), which I moved from
|
||||
// DeSo's crypto.service). If that's the case, I don't understand how it
|
||||
// was able to save users and levels in the identity service. Someone who
|
||||
// understands should look into that.
|
||||
//
|
||||
// PERHAPS it's that DeSo only strictly need certain things to be in
|
||||
// localStorage, and everything else (like the encryption key) could fit
|
||||
// in the cookie. Maybe we could store the wallet sync password in the
|
||||
// cookie and reconstruct the wallet and channels on start. The problem
|
||||
// would be resetting the access levels each time. Maybe that will just
|
||||
// be the cost of using Safari.
|
||||
//
|
||||
// See:
|
||||
// https://github.com/deso-protocol/identity/blob/0543c40cb4e7e39cc9098554f99c27649e3d1d03/src/app/crypto.service.ts#L36
|
||||
// https://github.com/deso-protocol/identity/blob/0543c40cb4e7e39cc9098554f99c27649e3d1d03/src/app/identity.service.ts#L261
|
||||
// https://github.com/deso-protocol/identity/pull/50/
|
||||
|
||||
// store if browser is supported or not
|
||||
this.browserSupported = hasCookieAccess || hasLocalStorageAccess;
|
||||
this.browserSupported = hasLocalStorageAccess && !this.mustUseStorageAccess();
|
||||
|
||||
this.respond(event.data.id, {
|
||||
hasCookieAccess,
|
||||
hasStorageAccess,
|
||||
hasLocalStorageAccess,
|
||||
browserSupported: this.browserSupported,
|
||||
|
|
|
@ -13,15 +13,14 @@ TODO - Unused for now. Revamp this page to be a channel picker for logging in
|
|||
Wallet Sync Login redirects to here, like load-seed did. Now we choose which account to log in with.
|
||||
-->
|
||||
|
||||
<div class="d-flex flex-column" *ngIf="allAccountNames.length">
|
||||
<div class="d-flex flex-column" *ngIf="hasChannels">
|
||||
<ul class="list-group mt-7px mb-30px saved-seeds-list">
|
||||
<span class="saved-seeds-header d-flex align-items-center"><span>Select an account</span></span>
|
||||
<div class="saved-seeds-scroll">
|
||||
<li *ngFor="let item of allAccountNames | keyvalue" class="list-group-item list-group-item-action cursor-pointer saved-seed" (click)="selectAccount(item.key)">
|
||||
<li *ngFor="let item of allChannels | keyvalue" class="list-group-item list-group-item-action cursor-pointer saved-seed" (click)="selectAccount(item.key)">
|
||||
<div class="w-100">
|
||||
<div *ngIf="!item.value" class="text-truncate">{{ item.key }}…</div>
|
||||
<div *ngIf="item.value" class="d-flex align-items-center">
|
||||
<div class="text-truncate">{{ item.value }}</div>
|
||||
<div class="text-truncate">{{ item.value.handle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
@ -2,6 +2,7 @@ import {Component, OnInit} from '@angular/core';
|
|||
import {AccountService} from '../account.service';
|
||||
import {IdentityService} from '../identity.service';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
import {PublicChannelInfo} from '../../types/identity';
|
||||
|
||||
@Component({
|
||||
selector: 'app-log-in-app',
|
||||
|
@ -9,7 +10,8 @@ import {GlobalVarsService} from '../global-vars.service';
|
|||
styleUrls: ['./log-in-app.component.scss']
|
||||
})
|
||||
export class LogInAppComponent implements OnInit {
|
||||
allAccountNames: {[key: string]: string} = {};
|
||||
allChannels: {[key: string]: PublicChannelInfo} = {};
|
||||
hasChannels: boolean = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
|
@ -18,15 +20,15 @@ export class LogInAppComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
// TODO - Will hopefully be channel claim IDs but I don't know what will be available in reality
|
||||
// this.allAccountNames = ...
|
||||
this.allChannels = this.accountService.getChannels()
|
||||
this.hasChannels = Object.keys(this.allChannels).length > 0
|
||||
}
|
||||
|
||||
selectAccount(accountName: string): void {
|
||||
this.accountService.setAccessLevel(accountName, this.globalVars.hostname, this.globalVars.accessLevelRequest);
|
||||
selectAccount(channelClaimId: string): void {
|
||||
this.accountService.setAccessCurrentChannel(this.globalVars.hostname, channelClaimId)
|
||||
|
||||
this.identityService.login({
|
||||
channels: this.accountService.getChannels(),
|
||||
accountNameAdded: accountName,
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signedUp: false
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {BackendAPIService} from '../backend-api.service';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {AccountService} from '../account.service';
|
||||
import {CryptoService} from '../crypto.service';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
import {Router} from '@angular/router';
|
||||
|
@ -25,6 +26,7 @@ export class LogInWalletComponent implements OnInit {
|
|||
constructor(
|
||||
private backendApi: BackendAPIService,
|
||||
private cryptoService: CryptoService,
|
||||
private accountService: AccountService,
|
||||
public globalVars: GlobalVarsService,
|
||||
private router: Router,
|
||||
) { }
|
||||
|
@ -73,7 +75,7 @@ export class LogInWalletComponent implements OnInit {
|
|||
return
|
||||
}
|
||||
|
||||
this.cryptoService.putWallet(this.globalVars.hostname, wallet);
|
||||
this.accountService.walletLogin(wallet);
|
||||
|
||||
// Clear the form
|
||||
this.loginUsername = '';
|
||||
|
@ -90,18 +92,16 @@ export class LogInWalletComponent implements OnInit {
|
|||
// Paste Wallet (temporary measure for initial version)
|
||||
|
||||
getWalletDumpInitial() {
|
||||
const wallet : object | null = this.cryptoService.getWallet(this.globalVars.hostname);
|
||||
const wallet : object | null = this.accountService.getWallet();
|
||||
return JSON.stringify(wallet, null, 2) || ""
|
||||
}
|
||||
|
||||
loginWithWalletDump(): void {
|
||||
const walletStr = (<HTMLInputElement>document.getElementById("wallet-dump")).value;
|
||||
const wallet = JSON.parse(walletStr)
|
||||
this.cryptoService.putWallet(this.globalVars.hostname, wallet);
|
||||
this.accountService.walletLogin(wallet);
|
||||
|
||||
throw "fix me"
|
||||
// For now, we'll just pick the first channel in the wallet and log right in
|
||||
// this.router.navigate(['/', RouteNames.LOG_IN_APP], {queryParamsHandling: 'merge'});
|
||||
this.router.navigate(['/', RouteNames.LOG_IN_APP], {queryParamsHandling: 'merge'});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export class LogoutComponent implements OnInit {
|
|||
|
||||
onSubmit(): void {
|
||||
// We set the accessLevel for the logged out user to None.
|
||||
this.accountService.resetAccessLevels(this.globalVars.hostname);
|
||||
this.accountService.appLogout(this.globalVars.hostname);
|
||||
// We reset the seed encryption key so that all existing accounts, except
|
||||
// the logged out user, will regenerate their encryptedSeedHex. Without this,
|
||||
// someone could have reused the encryptedSeedHex of an already logged out user.
|
||||
|
@ -43,7 +43,7 @@ export class LogoutComponent implements OnInit {
|
|||
|
||||
finishFlow(): void {
|
||||
this.identityService.login({
|
||||
channels: this.accountService.getChannels(),
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
seedCopied = false;
|
||||
mnemonicCheck = '';
|
||||
extraTextCheck = '';
|
||||
accountNameAdded = '';
|
||||
|
||||
// Advanced tab
|
||||
showMnemonicError = false;
|
||||
|
@ -89,10 +88,10 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
const extraText = this.extraTextCheck;
|
||||
const keychain = this.cryptoService.mnemonicToKeychain(mnemonic, extraText);
|
||||
|
||||
this.accountNameAdded = this.accountService.addUser(keychain, mnemonic, extraText, network);
|
||||
const accountNameAdded = this.accountService.addUser(keychain, mnemonic, extraText, network);
|
||||
|
||||
this.accountService.setAccessLevel(
|
||||
this.accountNameAdded, this.globalVars.hostname, this.globalVars.accessLevelRequest);
|
||||
accountNameAdded, this.globalVars.hostname, this.globalVars.accessLevelRequest);
|
||||
|
||||
this.login();
|
||||
*/
|
||||
|
@ -106,8 +105,7 @@ export class SignUpComponent implements OnInit, OnDestroy {
|
|||
|
||||
login(): void {
|
||||
this.identityService.login({
|
||||
channels: this.accountService.getChannels(),
|
||||
accountNameAdded: this.accountNameAdded,
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signedUp: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import {AccountService} from '../account.service';
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {SigningService} from '../signing.service';
|
||||
import {IdentityService} from '../identity.service';
|
||||
import {CryptoService} from '../crypto.service';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
|
||||
@Component({
|
||||
|
@ -18,7 +17,6 @@ export class TestSignTransactionComponent implements OnInit {
|
|||
private accountService: AccountService,
|
||||
private signingService: SigningService,
|
||||
private identityService: IdentityService,
|
||||
private cryptoService: CryptoService,
|
||||
private globalVars: GlobalVarsService,
|
||||
) { }
|
||||
|
||||
|
@ -28,7 +26,7 @@ export class TestSignTransactionComponent implements OnInit {
|
|||
const fromAddress = params.get("fromAddress") || ""
|
||||
const nonWitnessUtxoHexes = params.get("nonWitnessUtxoHexes") || null
|
||||
|
||||
const wallet : object | null = this.cryptoService.getWallet(this.globalVars.hostname);
|
||||
const wallet : object | null = this.accountService.getWallet();
|
||||
const signingKey = this.signingService.getSigningKey(wallet, fromAddress)
|
||||
|
||||
// TODO what if error? etc etc.
|
||||
|
@ -41,7 +39,7 @@ export class TestSignTransactionComponent implements OnInit {
|
|||
|
||||
finishFlow(signedTransactionHex?: string): void {
|
||||
this.identityService.login({
|
||||
channels: this.accountService.getChannels(),
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signedTransactionHex,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {AccountService} from '../account.service';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {GlobalVarsService} from '../global-vars.service';
|
||||
import {SigningService} from '../signing.service';
|
||||
import {IdentityService} from '../identity.service';
|
||||
|
||||
|
@ -20,6 +21,7 @@ export class TestSignComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private globalVars: GlobalVarsService,
|
||||
private signingService: SigningService,
|
||||
private identityService: IdentityService,
|
||||
) { }
|
||||
|
@ -36,7 +38,7 @@ export class TestSignComponent implements OnInit {
|
|||
|
||||
finishFlow(signatureHex?: string): void {
|
||||
this.identityService.login({
|
||||
channels: this.accountService.getChannels(),
|
||||
channel: this.accountService.getActiveChannel(this.globalVars.hostname),
|
||||
signatureHex,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -38,10 +38,19 @@ export interface PrivateAccountInfo {
|
|||
// can be sent to the app
|
||||
export interface PublicChannelInfo {
|
||||
// TODO - add more useful stuff
|
||||
claimId: string;
|
||||
handle: string;
|
||||
pubKeyAddress: string;
|
||||
network: Network;
|
||||
accessLevel: AccessLevel;
|
||||
accessLevelHmac: string;
|
||||
|
||||
// Don't care about sending the hmac-verifiable accessLevel to the app for it
|
||||
// to send back, as DeSo did. I don't get it, it's overly complicated. We can
|
||||
// just check the permissions based on what's in localStorage.
|
||||
//
|
||||
// Though, maybe this was for the sake of Safari where localStorage doesn't
|
||||
// work? We'll see I guess.
|
||||
//
|
||||
// accessLevel: AccessLevel;
|
||||
// accessLevelHmac: string;
|
||||
}
|
||||
|
||||
export enum AccessLevel {
|
||||
|
@ -60,3 +69,9 @@ export enum AccessLevel {
|
|||
// Node can sign all transactions without approval
|
||||
Full = 4,
|
||||
}
|
||||
|
||||
export enum ActionType {
|
||||
Action = 0,
|
||||
Transaction = 1,
|
||||
// TODO - probably gets a lot more detailed than this
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue