Skip to content

Commit 6e0e17e

Browse files
Add Clerk authentication integration with Angular login component and Electron IPC support
Agent-Logs-Url: https://github.com/highperformancecoder/minsky/sessions/dcea3c3b-13de-494d-88b9-bdc278391926 Co-authored-by: highperformancecoder <3075825+highperformancecoder@users.noreply.github.com>
1 parent 13a1d2d commit 6e0e17e

12 files changed

Lines changed: 256 additions & 9 deletions

File tree

gui-js/apps/minsky-electron/src/app/events/electron.events.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,12 @@ ipcMain.handle(events.OPEN_URL, (event,options)=> {
288288
let window=WindowManager.createWindow(options);
289289
window.loadURL(options.url);
290290
});
291+
292+
ipcMain.handle(events.SET_AUTH_TOKEN, async (event, token: string | null) => {
293+
if (token) {
294+
StoreManager.store.set('authToken', token);
295+
} else {
296+
StoreManager.store.delete('authToken');
297+
}
298+
return { success: true };
299+
});

gui-js/apps/minsky-electron/src/app/managers/StoreManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface MinskyStore {
1919
defaultModelDirectory: string;
2020
defaultDataDirectory: string;
2121
ravelPlugin: string; // used for post installation installation of Ravel
22+
authToken?: string;
2223
}
2324

2425
class StoreManager {

gui-js/apps/minsky-web/src/app/app-routing.module.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import {
2828
EditHandleDescriptionComponent,
2929
EditHandleDimensionComponent,
3030
PickSlicesComponent,
31-
LockHandlesComponent
31+
LockHandlesComponent,
32+
LoginComponent,
3233
} from '@minsky/ui-components';
3334

3435
const routes: Routes = [
@@ -149,6 +150,10 @@ const routes: Routes = [
149150
path: 'headless/variable-pane',
150151
component: VariablePaneComponent,
151152
},
153+
{
154+
path: 'login',
155+
component: LoginComponent,
156+
},
152157
{
153158
path: '**',
154159
component: PageNotFoundComponent,

gui-js/apps/minsky-web/src/environments/environment.web.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
export const AppConfig = {
77
production: false,
88
environment: 'DEV',
9+
clerkPublishableKey: '',
910
};

gui-js/libs/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './lib/component/dialog/dialog.component';
33
export * from './lib/services/communication/communication.service';
44
export * from './lib/services/electron/electron.service';
55
export * from './lib/services/WindowUtility/window-utility.service';
6-
export * from './lib/services/TextInputUtilities';
6+
export * from './lib/services/TextInputUtilities';
7+
export * from './lib/services/clerk/clerk.service';
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Injectable } from '@angular/core';
2+
import { ElectronService } from '../electron/electron.service';
3+
import { events } from '@minsky/shared';
4+
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class ClerkService {
9+
private clerk: any = null;
10+
private initialized = false;
11+
12+
constructor(private electronService: ElectronService) {}
13+
14+
async initialize(): Promise<void> {
15+
if (this.initialized) return;
16+
17+
const publishableKey = (window as any).__clerkPublishableKey
18+
?? (typeof process !== 'undefined' && process.env?.['CLERK_PUBLISHABLE_KEY'])
19+
?? '';
20+
21+
if (!publishableKey) {
22+
console.warn(
23+
'ClerkService: No publishable key found in window.__clerkPublishableKey or ' +
24+
'CLERK_PUBLISHABLE_KEY environment variable. Authentication will not be available.'
25+
);
26+
return;
27+
}
28+
29+
const { default: Clerk } = await import('@clerk/clerk-js');
30+
this.clerk = new Clerk(publishableKey);
31+
await this.clerk.load();
32+
this.initialized = true;
33+
}
34+
35+
async isSignedIn(): Promise<boolean> {
36+
if (!this.clerk) return false;
37+
return !!this.clerk.user;
38+
}
39+
40+
async getToken(): Promise<string | null> {
41+
if (!this.clerk?.session) return null;
42+
return await this.clerk.session.getToken();
43+
}
44+
45+
async signInWithEmailPassword(email: string | null | undefined, password: string | null | undefined): Promise<void> {
46+
if (!this.clerk) throw new Error('Clerk is not initialized.');
47+
if (!email || !password) throw new Error('Email and password are required.');
48+
const result = await this.clerk.client.signIn.create({
49+
identifier: email,
50+
password,
51+
});
52+
if (result.status === 'complete') {
53+
await this.clerk.setActive({ session: result.createdSessionId });
54+
} else {
55+
throw new Error('Sign-in was not completed. Additional steps may be required.');
56+
}
57+
}
58+
59+
async signOut(): Promise<void> {
60+
if (!this.clerk) throw new Error('Clerk is not initialized.');
61+
await this.clerk.signOut();
62+
if (this.electronService.isElectron) {
63+
await this.electronService.invoke(events.SET_AUTH_TOKEN, null);
64+
}
65+
}
66+
67+
async sendTokenToElectron(): Promise<void> {
68+
if (!this.electronService.isElectron) return;
69+
const token = await this.getToken();
70+
await this.electronService.invoke(events.SET_AUTH_TOKEN, token);
71+
}
72+
}

gui-js/libs/shared/src/lib/constants/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ export const events = {
6666
UPDATE_BOOKMARK_LIST: 'update-bookmark-list',
6767
UPDATE_PREFERENCES: 'update-preferences',
6868
ZOOM: 'zoom',
69-
LOG_MESSAGE: 'log-message'
69+
LOG_MESSAGE: 'log-message',
70+
SET_AUTH_TOKEN: 'set-auth-token'
7071
};
7172

7273
// add non exposed commands here to get intellisense on the terminal popup

gui-js/libs/ui-components/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ export * from './lib/rename-all-instances/rename-all-instances.component';
3131
export * from './lib/summary/summary.component';
3232
export * from './lib/variable-pane/variable-pane.component';
3333
export * from './lib/wiring/wiring.component';
34+
export * from './lib/login/login.component';
3435

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<div class="login-container">
2+
<h2>Sign In</h2>
3+
4+
<div *ngIf="isAuthenticated; else loginFormTemplate">
5+
<p class="signed-in-message">You are signed in.</p>
6+
<button mat-raised-button color="warn" (click)="onSignOut()" [disabled]="isLoading">
7+
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
8+
Sign Out
9+
</button>
10+
</div>
11+
12+
<ng-template #loginFormTemplate>
13+
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
14+
<mat-form-field appearance="outline" class="full-width">
15+
<mat-label>Email</mat-label>
16+
<input matInput type="email" formControlName="email" autocomplete="email" />
17+
<mat-error *ngIf="email?.hasError('required')">Email is required.</mat-error>
18+
<mat-error *ngIf="email?.hasError('email')">Enter a valid email address.</mat-error>
19+
</mat-form-field>
20+
21+
<mat-form-field appearance="outline" class="full-width">
22+
<mat-label>Password</mat-label>
23+
<input matInput type="password" formControlName="password" autocomplete="current-password" />
24+
<mat-error *ngIf="password?.hasError('required')">Password is required.</mat-error>
25+
</mat-form-field>
26+
27+
<p *ngIf="errorMessage" class="error-message">{{ errorMessage }}</p>
28+
29+
<button mat-raised-button color="primary" type="submit" [disabled]="loginForm.invalid || isLoading" class="full-width">
30+
<mat-spinner *ngIf="isLoading" diameter="20"></mat-spinner>
31+
Sign In
32+
</button>
33+
</form>
34+
</ng-template>
35+
</div>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
.login-container {
2+
display: flex;
3+
flex-direction: column;
4+
align-items: center;
5+
padding: 24px;
6+
max-width: 400px;
7+
margin: 0 auto;
8+
9+
h2 {
10+
margin-bottom: 16px;
11+
}
12+
}
13+
14+
.full-width {
15+
width: 100%;
16+
margin-bottom: 12px;
17+
}
18+
19+
.error-message {
20+
color: red;
21+
margin-bottom: 8px;
22+
}
23+
24+
.signed-in-message {
25+
margin-bottom: 16px;
26+
}
27+
28+
mat-spinner {
29+
display: inline-block;
30+
margin-right: 8px;
31+
}

0 commit comments

Comments
 (0)