Add nostr-login

This commit is contained in:
Milad Raeisi
2024-10-23 20:48:28 +04:00
parent 5dc855e22a
commit f6cca09cf3
7 changed files with 3232 additions and 2679 deletions

5204
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,8 @@
"@angular/pwa": "^18.2.8",
"@angular/router": "18.2.8",
"@angular/service-worker": "^18.2.8",
"@blockcore/nostr-login": "^1.0.6",
"@blockcore/nostr-login": "^1.0.7",
"@blockcore/nostr-login-components": "^1.0.7",
"@ctrl/ngx-emoji-mart": "^9.2.0",
"@gandlaf21/bolt11-decode": "^3.1.1",
"@getalby/lightning-tools": "^5.0.3",
@@ -50,7 +51,7 @@
"blurhash": "^2.0.5",
"buffer": "^6.0.3",
"cropperjs": "^1.6.2",
"crypto-browserify": "^3.12.0",
"crypto-browserify": "^3.3.0",
"crypto-js": "4.2.0",
"dayjs": "^1.11.13",
"dompurify": "^3.1.7",
@@ -58,7 +59,7 @@
"install": "^0.13.0",
"jsdom": "^25.0.1",
"light-bolt11-decoder": "^3.2.0",
"lnd-grpc": "^0.5.4",
"lnd-grpc": "^0.4.6",
"localforage": "^1.10.0",
"lodash-es": "4.17.21",
"luxon": "3.5.0",
@@ -81,7 +82,7 @@
"zone.js": "0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "18.2.8",
"@angular-devkit/build-angular": "^18.2.9",
"@angular/cli": "18.2.8",
"@angular/compiler-cli": "18.2.8",
"@tailwindcss/typography": "0.5.15",

View File

@@ -11,277 +11,317 @@
>
Login
</div>
<div class="mt-0.5 flex items-baseline font-medium">
<div>Don't have an account?</div>
<a
class="ml-1 text-primary-500 hover:underline"
[routerLink]="['/register']"
>Register</a
<div *ngIf="!useNostrLogin">
<div class="mt-0.5 flex items-baseline font-medium">
<div>Don't have an account?</div>
<a
class="ml-1 text-primary-500 hover:underline"
[routerLink]="['/register']"
>Register</a
>
</div>
<angor-alert
*ngIf="showSecAlert"
class="mt-8"
[appearance]="'outline'"
[showIcon]="false"
[type]="secAlert.type"
[@shake]="secAlert.type === 'error'"
>
</div>
{{ secAlert.message }}
</angor-alert>
<angor-alert
*ngIf="showSecAlert"
class="mt-8"
[appearance]="'outline'"
[showIcon]="false"
[type]="secAlert.type"
[@shake]="secAlert.type === 'error'"
>
{{ secAlert.message }}
</angor-alert>
<div>
<!-- Separator -->
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">
Login with extension
</div>
<div class="mt-px flex-auto border-t"></div>
</div>
<!-- extension login buttons -->
<div class="mt-8 flex items-center space-x-4">
<button
class="flex-auto space-x-2"
type="button"
mat-stroked-button
(click)="loginWithNostrExtension()"
>
<mat-icon
class="icon-size-5"
[svgIcon]="'feather:zap'"
></mat-icon>
<span>Login with Nostr Extension</span>
</button>
</div>
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">Or</div>
<div class="mt-px flex-auto border-t"></div>
</div>
</div>
<!-- Login form with Secret Key -->
<form
class="mt-8"
[formGroup]="SecretKeyLoginForm"
(ngSubmit)="loginWithSecretKey()"
>
<!-- secret key field -->
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">Enter secret key</div>
<div class="mt-px flex-auto border-t"></div>
</div>
<mat-form-field class="w-full">
<mat-label>Secret Key</mat-label>
<input
matInput
formControlName="secretKey"
autocomplete="secretKey"
/>
@if (
SecretKeyLoginForm.get('secretKey').hasError(
'required'
)
) {
<mat-error> Secret key is required </mat-error>
}
</mat-form-field>
<!-- Password field -->
<mat-form-field class="w-full">
<mat-label>Password</mat-label>
<input
matInput
type="password"
[formControlName]="'password'"
autocomplete="current-password-seckey"
#secretPasswordField
/>
<button
mat-icon-button
type="button"
(click)="
secretPasswordField.type === 'password'
? (secretPasswordField.type = 'text')
: (secretPasswordField.type = 'password')
"
matSuffix
>
<mat-icon
*ngIf="secretPasswordField.type === 'password'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye'"
></mat-icon>
<mat-icon
*ngIf="secretPasswordField.type === 'text'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye-slash'"
></mat-icon>
</button>
<mat-error
*ngIf="
SecretKeyLoginForm.get('password').hasError(
'required'
)
"
>
Password is required
</mat-error>
</mat-form-field>
<!-- Submit button -->
<button
class="angor-mat-button-large mt-6 w-full"
mat-flat-button
color="primary"
[disabled]="SecretKeyLoginForm.invalid"
>
<span *ngIf="!loading">Login</span>
<mat-progress-spinner
*ngIf="loading"
diameter="24"
mode="indeterminate"
></mat-progress-spinner>
</button>
</form>
<div *ngIf="isInstalledExtension">
<!-- Separator -->
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">Login with extension</div>
<div class="text-secondary mx-2">Or enter menemonic</div>
<div class="mt-px flex-auto border-t"></div>
</div>
<!-- extension login buttons -->
<angor-alert
*ngIf="showMenemonicAlert"
class="mt-8"
[appearance]="'outline'"
[showIcon]="false"
[type]="menemonicAlert.type"
[@shake]="menemonicAlert.type === 'error'"
>
{{ menemonicAlert.message }}
</angor-alert>
<!-- Login form with Menemonic -->
<form
class="mt-8"
[formGroup]="MenemonicLoginForm"
(ngSubmit)="loginWithMenemonic()"
>
<!-- Menemonic field -->
<mat-form-field class="w-full">
<mat-label>Menemonic</mat-label>
<input
matInput
formControlName="menemonic"
autocomplete="menemonic"
/>
@if (
MenemonicLoginForm.get('menemonic').hasError(
'required'
)
) {
<mat-error> Menemonic is required </mat-error>
}
</mat-form-field>
<!-- Passphrase field -->
<mat-form-field class="w-full">
<mat-label>Passphrase (Optional)</mat-label>
<input
matInput
type="password"
[formControlName]="'passphrase'"
autocomplete="current-passphrase-menemonic"
#passphraseField
/>
<button
mat-icon-button
type="button"
(click)="
passphraseField.type === 'password'
? (passphraseField.type = 'text')
: (passphraseField.type = 'password')
"
matSuffix
>
<mat-icon
*ngIf="passphraseField.type === 'password'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye'"
></mat-icon>
<mat-icon
*ngIf="passphraseField.type === 'text'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye-slash'"
></mat-icon>
</button>
<mat-error
*ngIf="
MenemonicLoginForm.get('passphrase').hasError(
'required'
)
"
>
Passphrase is required
</mat-error>
</mat-form-field>
<!-- Password field -->
<mat-form-field class="w-full">
<mat-label>Password</mat-label>
<input
matInput
type="password"
[formControlName]="'password'"
autocomplete="current-password-menemonic"
#menemonicPasswordField
/>
<button
mat-icon-button
type="button"
(click)="
menemonicPasswordField.type === 'password'
? (menemonicPasswordField.type = 'text')
: (menemonicPasswordField.type = 'password')
"
matSuffix
>
<mat-icon
*ngIf="
menemonicPasswordField.type === 'password'
"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye'"
></mat-icon>
<mat-icon
*ngIf="menemonicPasswordField.type === 'text'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye-slash'"
></mat-icon>
</button>
<mat-error
*ngIf="
MenemonicLoginForm.get('password').hasError(
'required'
)
"
>
Password is required
</mat-error>
</mat-form-field>
<!-- Submit button -->
<button
class="angor-mat-button-large mt-6 w-full"
mat-flat-button
color="primary"
[disabled]="MenemonicLoginForm.invalid"
>
<span *ngIf="!loading">Login</span>
<mat-progress-spinner
*ngIf="loading"
diameter="24"
mode="indeterminate"
></mat-progress-spinner>
</button>
</form>
</div>
<div *ngIf="useNostrLogin">
<div class="mt-8 flex items-center space-x-4">
<button
class="flex-auto space-x-2"
type="button"
mat-stroked-button
(click)="loginWithNostrExtension()"
(click)="loginWithNostrAccount()"
>
<mat-icon
class="icon-size-5"
[svgIcon]="'feather:zap'"
></mat-icon>
<span>Login with Nostr Extension</span>
<span>Login with Nostr Account</span>
</button>
</div>
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">Or</div>
<div class="mt-px flex-auto border-t"></div>
<div class="mt-8 flex items-center space-x-4">
<button
class="flex-auto space-x-2"
type="button"
mat-stroked-button
(click)="signUpWithNostrAccount()"
>
<mat-icon
class="icon-size-5"
[svgIcon]="'feather:zap'"
></mat-icon>
<span>Sign up with Nostr Account</span>
</button>
</div>
</div>
<!-- Login form with Secret Key -->
<form
class="mt-8"
[formGroup]="SecretKeyLoginForm"
(ngSubmit)="loginWithSecretKey()"
>
<!-- secret key field -->
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">Enter secret key</div>
<div class="mt-px flex-auto border-t"></div>
</div>
<mat-form-field class="w-full">
<mat-label>Secret Key</mat-label>
<input
matInput
formControlName="secretKey"
autocomplete="secretKey"
/>
@if (
SecretKeyLoginForm.get('secretKey').hasError('required')
) {
<mat-error> Secret key is required </mat-error>
}
</mat-form-field>
<!-- Password field -->
<mat-form-field class="w-full">
<mat-label>Password</mat-label>
<input
matInput
type="password"
[formControlName]="'password'"
autocomplete="current-password-seckey"
#secretPasswordField
/>
<button
mat-icon-button
type="button"
(click)="
secretPasswordField.type === 'password'
? (secretPasswordField.type = 'text')
: (secretPasswordField.type = 'password')
"
matSuffix
>
<mat-icon
*ngIf="secretPasswordField.type === 'password'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye'"
></mat-icon>
<mat-icon
*ngIf="secretPasswordField.type === 'text'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye-slash'"
></mat-icon>
</button>
<mat-error
*ngIf="
SecretKeyLoginForm.get('password').hasError(
'required'
)
"
>
Password is required
</mat-error>
</mat-form-field>
<!-- Submit button -->
<button
class="angor-mat-button-large mt-6 w-full"
mat-flat-button
color="primary"
[disabled]="SecretKeyLoginForm.invalid"
>
<span *ngIf="!loading">Login</span>
<mat-progress-spinner
*ngIf="loading"
diameter="24"
mode="indeterminate"
></mat-progress-spinner>
</button>
</form>
<div class="mt-8 flex items-center">
<div class="mt-px flex-auto border-t"></div>
<div class="text-secondary mx-2">Or enter menemonic</div>
<div class="mt-px flex-auto border-t"></div>
</div>
<angor-alert
*ngIf="showMenemonicAlert"
class="mt-8"
[appearance]="'outline'"
[showIcon]="false"
[type]="menemonicAlert.type"
[@shake]="menemonicAlert.type === 'error'"
>
{{ menemonicAlert.message }}
</angor-alert>
<!-- Login form with Menemonic -->
<form
class="mt-8"
[formGroup]="MenemonicLoginForm"
(ngSubmit)="loginWithMenemonic()"
>
<!-- Menemonic field -->
<mat-form-field class="w-full">
<mat-label>Menemonic</mat-label>
<input
matInput
formControlName="menemonic"
autocomplete="menemonic"
/>
@if (
MenemonicLoginForm.get('menemonic').hasError('required')
) {
<mat-error> Menemonic is required </mat-error>
}
</mat-form-field>
<!-- Passphrase field -->
<mat-form-field class="w-full">
<mat-label>Passphrase (Optional)</mat-label>
<input
matInput
type="password"
[formControlName]="'passphrase'"
autocomplete="current-passphrase-menemonic"
#passphraseField
/>
<button
mat-icon-button
type="button"
(click)="
passphraseField.type === 'password'
? (passphraseField.type = 'text')
: (passphraseField.type = 'password')
"
matSuffix
>
<mat-icon
*ngIf="passphraseField.type === 'password'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye'"
></mat-icon>
<mat-icon
*ngIf="passphraseField.type === 'text'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye-slash'"
></mat-icon>
</button>
<mat-error
*ngIf="
MenemonicLoginForm.get('passphrase').hasError(
'required'
)
"
>
Passphrase is required
</mat-error>
</mat-form-field>
<!-- Password field -->
<mat-form-field class="w-full">
<mat-label>Password</mat-label>
<input
matInput
type="password"
[formControlName]="'password'"
autocomplete="current-password-menemonic"
#menemonicPasswordField
/>
<button
mat-icon-button
type="button"
(click)="
menemonicPasswordField.type === 'password'
? (menemonicPasswordField.type = 'text')
: (menemonicPasswordField.type = 'password')
"
matSuffix
>
<mat-icon
*ngIf="menemonicPasswordField.type === 'password'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye'"
></mat-icon>
<mat-icon
*ngIf="menemonicPasswordField.type === 'text'"
class="icon-size-5"
[svgIcon]="'heroicons_solid:eye-slash'"
></mat-icon>
</button>
<mat-error
*ngIf="
MenemonicLoginForm.get('password').hasError(
'required'
)
"
>
Password is required
</mat-error>
</mat-form-field>
<!-- Submit button -->
<button
class="angor-mat-button-large mt-6 w-full"
mat-flat-button
color="primary"
[disabled]="MenemonicLoginForm.invalid"
>
<span *ngIf="!loading">Login</span>
<mat-progress-spinner
*ngIf="loading"
diameter="24"
mode="indeterminate"
></mat-progress-spinner>
</button>
</form>
</div>
</div>
<div

View File

@@ -17,7 +17,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Router, RouterLink } from '@angular/router';
import { SignerService } from 'app/services/signer.service';
import { StateService } from 'app/services/state.service';
import { init as initNostrLogin, launch as launchNostrLoginDialog } from '@blockcore/nostr-login';
@Component({
selector: 'auth-sign-in',
templateUrl: './login.component.html',
@@ -34,7 +34,7 @@ import { StateService } from 'app/services/state.service';
MatCheckboxModule,
MatProgressSpinnerModule,
CommonModule,
],
],
})
export class LoginComponent implements OnInit {
SecretKeyLoginForm: FormGroup;
@@ -52,6 +52,8 @@ export class LoginComponent implements OnInit {
npub: string = '';
nsec: string = '';
useNostrLogin = false;
constructor(
private _formBuilder: FormBuilder,
private _router: Router,
@@ -60,8 +62,32 @@ export class LoginComponent implements OnInit {
) {}
ngOnInit(): void {
this.initializeForms();
this.checkNostrExtensionAvailability();
initNostrLogin({
theme: 'ocean',
noBanner: true,
title:'Angor Hub',
onAuth: (npub: string, options: any) => {
console.log('User authenticated:', npub);
alert('User authenticated: ' + npub);
},
});
}
private async loginWithNostrAccount(): Promise<void> {
launchNostrLoginDialog(
'welcome-login',
);
}
private async signUpWithNostrAccount(): Promise<void> {
launchNostrLoginDialog(
'welcome-signup',
);
}
private async initializeAppState(): Promise<void> {

View File

@@ -145,42 +145,48 @@ export class SignerService {
}
//seckey===============
async setSecretKey(secretKey: string, password: string) {
const encryptedSecretKey = await this.securityService.encryptData(
secretKey,
password
);
localStorage.setItem(
this.localStorageSecretKeyName,
encryptedSecretKey
);
async setSecretKey(secretKey: string, password: string = "") {
if (password === "") {
localStorage.setItem(this.localStorageSecretKeyName, secretKey);
localStorage.setItem('usePassword', 'false');
} else {
const encryptedSecretKey = await this.securityService.encryptData(secretKey, password);
localStorage.setItem(this.localStorageSecretKeyName, encryptedSecretKey);
localStorage.setItem('usePassword', 'true');
}
}
async getSecretKey(password: string) {
const encryptedSecretKey = localStorage.getItem(
this.localStorageSecretKeyName
);
async getSecretKey(password: string = "") {
const encryptedSecretKey = localStorage.getItem(this.localStorageSecretKeyName);
const usePassword = localStorage.getItem('usePassword') === 'true';
if (!encryptedSecretKey) {
return null;
}
return await this.securityService.decryptData(
encryptedSecretKey,
password
);
if (!usePassword) {
return encryptedSecretKey;
}
return await this.securityService.decryptData(encryptedSecretKey, password);
}
async getDecryptedSecretKey(): Promise<string | null> {
try {
const storedPassword = this.getPassword(); // Ensure this retrieves a valid password
if (storedPassword) {
return await this.getSecretKey(storedPassword); // Ensure getSecretKey returns a valid private key
const usePassword = localStorage.getItem('usePassword') === 'true';
if (!usePassword) {
return this.getSecretKey();
}
const result = await this.requestPassword(); // Prompt user for password if not stored
const storedPassword = this.getPassword();
if (storedPassword) {
return await this.getSecretKey(storedPassword);
}
const result = await this.requestPassword();
if (result?.password) {
const decryptedPrivateKey = await this.getSecretKey(
result.password
); // Check that the private key is decrypted properly
const decryptedPrivateKey = await this.getSecretKey(result.password);
if (result.duration !== 0) {
this.savePassword(result.password, result.duration);
}
@@ -195,28 +201,41 @@ export class SignerService {
}
}
//nsec===============
async setNsec(nsec: string, password: string) {
const encryptedNsec = await this.securityService.encryptData(
nsec,
password
);
localStorage.setItem(this.localStorageNsecName, encryptedNsec);
async setNsec(nsec: string, password: string = "") {
if (password === "") {
localStorage.setItem(this.localStorageNsecName, nsec);
localStorage.setItem('usePassword', 'false');
} else {
const encryptedNsec = await this.securityService.encryptData(nsec, password);
localStorage.setItem(this.localStorageNsecName, encryptedNsec);
localStorage.setItem('usePassword', 'true');
}
}
async getNsec(password: string) {
async getNsec(password: string = "") {
const encryptedNsec = localStorage.getItem(this.localStorageNsecName);
const usePassword = localStorage.getItem('usePassword') === 'true';
if (!encryptedNsec) {
return null;
}
return await this.securityService.decryptData(encryptedNsec, password);
if (!usePassword) {
return encryptedNsec;
}
return await this.securityService.decryptData(encryptedNsec, password);
}
setPublicKeyFromExtension(publicKey: string) {
this.setPublicKey(publicKey);
}
handleLoginWithKey(key: string, password: string): boolean {
handleLoginWithKey(key: string, password: string=""): boolean {
let secretKey: string;
let pubkey: string;
let nsec: string;

View File

@@ -89,14 +89,6 @@ export class Utilities {
}
}
ensureHexIdentifier(pubkey: string) {
if (pubkey.startsWith('npub')) {
pubkey = this.arrayToHex(this.convertFromBech32(pubkey));
}
return pubkey;
}
copy(text: string) {
this.copyToClipboard(text);
@@ -170,25 +162,6 @@ export class Utilities {
return bytesToHex(value);
}
convertFromBech32(address: string) {
const decoded = bech32.decode(address);
const key = bech32.fromWords(decoded.words);
return key;
}
convertFromBech32ToHex(address: string) {
const decoded = bech32.decode(address);
const key = bech32.fromWords(decoded.words);
return this.arrayToHex(key);
}
convertBech32ToText(str: string) {
const decoded = bech32.decode(str, 1000);
const buf = bech32.fromWords(decoded.words);
return new TextDecoder().decode(Uint8Array.from(buf));
}
keyToHex(publicKey: Uint8Array) {
return bytesToHex(publicKey);
}

View File

@@ -1,21 +1,31 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./src",
"outDir": "./dist/out-tsc",
"paths": {
"@blockcore/*": ["node_modules/@blockcore/*"]
},
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"declaration": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": ["ES2022", "dom"]
"lib": ["ES2022", "dom"],
"strict": false,
"noImplicitOverride": false,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": false,
"skipLibCheck": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": false,
"strictInputAccessModifiers": false,
"strictTemplates": false
}
}