Skip to content

Commit 5115c5b

Browse files
committed
Add api keys
1 parent c1b07cc commit 5115c5b

5 files changed

Lines changed: 302 additions & 89 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openworkers-dash",
3-
"version": "1.1.12",
3+
"version": "1.1.13",
44
"scripts": {
55
"ng": "ng",
66
"dev": "ng serve --host 127.0.0.1",

src/app/app.component.html

Lines changed: 48 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,53 @@
1-
@if (user$ | async; as user) {
2-
@if (menuActive$ | async; as menuActive) {
3-
<nav class="main-nav border-b">
4-
<div class="nav-content">
5-
<div class="flex gap-8 flex-1">
6-
<!-- Logo -->
7-
<div class="flex flex-col min-w-max uppercase justify-center text-4xl font-extrabold">
8-
<div class="flex gap-2 flex-1 p-4">
9-
<a routerLink="/" href="/">
10-
<svg class="h-8 w-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46">
11-
<path
12-
class="fill-black dark:fill-white"
13-
d="m205.14,205.14l-95.11-95.12,31.71-31.71,63.41,63.41,63.38-63.39C245.26,31.89,197.22,0,141.73,0S38.21,31.89,14.94,78.34l95.09,95.1-31.7,31.7L.71,127.52c-.47,4.67-.71,9.41-.71,14.21,0,78.28,63.46,141.73,141.73,141.73s141.73-63.46,141.73-141.73c0-4.8-.24-9.54-.71-14.21l-77.62,77.62Z"
14-
/>
15-
</svg>
16-
</a>
17-
</div>
18-
</div>
19-
<!-- Menu -->
20-
<div class="flex gap-4 mb-[-1px]">
21-
<!-- Workers -->
22-
<a class="menu-item" routerLink="/workers" [ngClass]="{ active: menuActive === 'workers' }"> Workers </a>
23-
<!-- Environments -->
24-
<a class="menu-item" routerLink="/environments" [ngClass]="{ active: menuActive === 'environments' }">
25-
Environments
26-
</a>
27-
<!-- Databases -->
28-
<a class="menu-item" routerLink="/databases" [ngClass]="{ active: menuActive === 'databases' }">
29-
Databases
30-
</a>
31-
<!-- KV -->
32-
<a class="menu-item" routerLink="/kv-namespaces" [ngClass]="{ active: menuActive === 'kv' }">
33-
KV
34-
</a>
35-
<!-- Storage -->
36-
<a class="menu-item" routerLink="/storage-configs" [ngClass]="{ active: menuActive === 'storage' }">
37-
Storage
38-
</a>
39-
</div>
40-
</div>
41-
<div class="flex gap-4">
42-
<div class="menu-item">
43-
<app-theme-switch class="flex"></app-theme-switch>
44-
</div>
45-
<div>
46-
<a routerLink="/account" [ngClass]="{ active: menuActive === 'account' }">
47-
@if (user.avatarUrl) {
48-
<img class="rounded-full w-8 min-h-8" [src]="user.avatarUrl" [alt]="user.username" />
49-
} @else {
50-
<ng-icon name="heroUser" class="w-8 h-8" />
51-
}
52-
</a>
53-
</div>
1+
@if (user$ | async; as user) { @if (menuActive$ | async; as menuActive) {
2+
<nav class="main-nav border-b">
3+
<div class="nav-content">
4+
<div class="flex gap-8 flex-1">
5+
<!-- Logo -->
6+
<div class="flex flex-col min-w-max uppercase justify-center text-4xl font-extrabold">
7+
<div class="flex gap-2 flex-1 p-4">
8+
<a routerLink="/" href="/">
9+
<svg class="h-8 w-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 283.46 283.46">
10+
<path
11+
class="fill-black dark:fill-white"
12+
d="m205.14,205.14l-95.11-95.12,31.71-31.71,63.41,63.41,63.38-63.39C245.26,31.89,197.22,0,141.73,0S38.21,31.89,14.94,78.34l95.09,95.1-31.7,31.7L.71,127.52c-.47,4.67-.71,9.41-.71,14.21,0,78.28,63.46,141.73,141.73,141.73s141.73-63.46,141.73-141.73c0-4.8-.24-9.54-.71-14.21l-77.62,77.62Z"
13+
/>
14+
</svg>
15+
</a>
5416
</div>
5517
</div>
56-
</nav>
57-
}
58-
}
18+
<!-- Menu -->
19+
<div class="flex gap-4 mb-[-1px]">
20+
<!-- Workers -->
21+
<a class="menu-item" routerLink="/workers" [ngClass]="{ active: menuActive === 'workers' }"> Workers </a>
22+
<!-- Environments -->
23+
<a class="menu-item" routerLink="/environments" [ngClass]="{ active: menuActive === 'environments' }">
24+
Environments
25+
</a>
26+
<!-- Databases -->
27+
<a class="menu-item" routerLink="/databases" [ngClass]="{ active: menuActive === 'databases' }"> Databases </a>
28+
<!-- KV -->
29+
<a class="menu-item" routerLink="/kv-namespaces" [ngClass]="{ active: menuActive === 'kv' }"> KV </a>
30+
<!-- Storage -->
31+
<a class="menu-item" routerLink="/storage-configs" [ngClass]="{ active: menuActive === 'storage' }">
32+
Storage
33+
</a>
34+
</div>
35+
</div>
36+
<div class="flex gap-4 items-center">
37+
<div class="menu-item">
38+
<app-theme-switch class="flex"></app-theme-switch>
39+
</div>
40+
<a routerLink="/account" [ngClass]="{ active: menuActive === 'account' }">
41+
@if (user.avatarUrl) {
42+
<img class="rounded-full w-8 min-h-8" [src]="user.avatarUrl" [alt]="user.username" />
43+
} @else {
44+
<ng-icon name="heroUser" class="w-8 h-8" />
45+
}
46+
</a>
47+
</div>
48+
</div>
49+
</nav>
50+
} }
5951

6052
<main>
6153
<router-outlet></router-outlet>
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<div class="page">
2+
<h3 class="title">Account</h3>
3+
4+
@if (user$ | async; as user) {
5+
<!-- Profile Card -->
6+
<div class="card shadow-sm mt-6">
7+
<div class="p-4">
8+
<h4 class="mb-4">Profile</h4>
9+
<div class="flex flex-col gap-2">
10+
@if (user.avatarUrl) {
11+
<div class="flex items-center gap-3">
12+
<img [src]="user.avatarUrl" class="w-12 h-12 rounded-full" alt="Avatar" />
13+
</div>
14+
}
15+
<div>
16+
<label class="text-sm text-gray-500">Username</label>
17+
<div class="font-mono text-sm bg-gray-100 dark:bg-gray-800 p-2 rounded mt-1">
18+
{{ user.username }}
19+
</div>
20+
</div>
21+
</div>
22+
</div>
23+
</div>
24+
25+
<!-- Limits Card -->
26+
<div class="card shadow-sm mt-6">
27+
<div class="p-4">
28+
<h4 class="mb-4">Resource Limits</h4>
29+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
30+
<div class="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded">
31+
<div class="text-2xl font-bold text-blue">{{ user.limits.workers }}</div>
32+
<div class="text-sm text-gray-500">Workers</div>
33+
</div>
34+
<div class="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded">
35+
<div class="text-2xl font-bold text-blue">{{ user.limits.environments }}</div>
36+
<div class="text-sm text-gray-500">Environments</div>
37+
</div>
38+
<div class="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded">
39+
<div class="text-2xl font-bold text-blue">{{ user.limits.databases }}</div>
40+
<div class="text-sm text-gray-500">Databases</div>
41+
</div>
42+
<div class="text-center p-3 bg-gray-50 dark:bg-gray-800 rounded">
43+
<div class="text-2xl font-bold text-blue">{{ user.limits.kv }}</div>
44+
<div class="text-sm text-gray-500">KV Namespaces</div>
45+
</div>
46+
</div>
47+
</div>
48+
</div>
49+
50+
<!-- API Keys Card -->
51+
<div class="card shadow-sm mt-6">
52+
<div class="p-4">
53+
<div class="flex justify-between items-center mb-4">
54+
<h4>API Keys</h4>
55+
<button class="btn-blue text-sm" (click)="showCreateKey = true" [disabled]="showCreateKey">Create Key</button>
56+
</div>
57+
58+
<!-- Create Key Form -->
59+
@if (showCreateKey) {
60+
<div class="bg-gray-50 dark:bg-gray-800 p-4 rounded mb-4">
61+
<form (ngSubmit)="createKey()">
62+
<div class="flex gap-2">
63+
<input
64+
type="text"
65+
[(ngModel)]="newKeyName"
66+
name="keyName"
67+
placeholder="Key name (e.g., CLI, CI/CD)"
68+
class="input-outline flex-1"
69+
required
70+
/>
71+
<button type="submit" class="btn-blue" [disabled]="!newKeyName || (creating$ | async)">
72+
@if (creating$ | async) { Creating... } @else { Create }
73+
</button>
74+
<button type="button" class="btn-outline" (click)="cancelCreate()">Cancel</button>
75+
</div>
76+
</form>
77+
</div>
78+
}
79+
80+
<!-- New Key Display -->
81+
@if (newKeyToken) {
82+
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 p-4 rounded mb-4">
83+
<p class="text-sm text-green-800 dark:text-green-200 mb-2">
84+
Copy your API key now. You won't be able to see it again!
85+
</p>
86+
<div class="flex gap-2">
87+
<code class="flex-1 bg-white dark:bg-gray-900 p-2 rounded text-sm font-mono break-all">
88+
{{ newKeyToken }}
89+
</code>
90+
<button class="btn-outline text-sm" (click)="copyToken()">Copy</button>
91+
</div>
92+
<button class="text-sm text-green-700 dark:text-green-300 mt-2" (click)="newKeyToken = null">Done</button>
93+
</div>
94+
}
95+
96+
<!-- Usage Example -->
97+
<div class="bg-gray-100 dark:bg-gray-800 p-3 rounded mb-4">
98+
<p class="text-xs text-gray-500 mb-2">Usage example:</p>
99+
<code class="text-xs font-mono break-all">
100+
curl -H "Authorization: Bearer $OW_TOKEN" https://dash.openworkers.com/api/v1/profile | jq
101+
</code>
102+
</div>
103+
104+
<!-- Keys List -->
105+
@if (keys$ | async; as keys) { @if (keys.length === 0 && !showCreateKey) {
106+
<p class="text-gray-500 text-sm">No API keys yet. Create one to access the API programmatically.</p>
107+
} @else {
108+
<div class="overflow-x-auto">
109+
<table class="w-full text-sm">
110+
<thead>
111+
<tr class="text-left text-gray-500 border-b dark:border-gray-700">
112+
<th class="pb-2">Name</th>
113+
<th class="pb-2">Key</th>
114+
<th class="pb-2">Last Used</th>
115+
<th class="pb-2">Created</th>
116+
<th class="pb-2"></th>
117+
</tr>
118+
</thead>
119+
<tbody>
120+
@for (key of keys; track key.id) {
121+
<tr class="border-b dark:border-gray-700">
122+
<td class="py-2">{{ key.name }}</td>
123+
<td class="py-2 font-mono text-gray-500">{{ key.tokenPrefix }}...</td>
124+
<td class="py-2 text-gray-500">
125+
{{ key.lastUsedAt ? (key.lastUsedAt | date : 'short') : 'Never' }}
126+
</td>
127+
<td class="py-2 text-gray-500">{{ key.createdAt | date : 'short' }}</td>
128+
<td class="py-2">
129+
<button class="text-red hover:underline text-sm" (click)="deleteKey(key.id)">Delete</button>
130+
</td>
131+
</tr>
132+
}
133+
</tbody>
134+
</table>
135+
</div>
136+
} }
137+
</div>
138+
</div>
139+
140+
<!-- Logout -->
141+
<div class="mt-8">
142+
<button class="btn-outline" (click)="logout()">Logout</button>
143+
</div>
144+
}
145+
</div>

src/app/modules/account/account.page.ts

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,71 @@
11
import { CommonModule } from '@angular/common';
2-
import { Component } from '@angular/core';
2+
import { Component, OnInit } from '@angular/core';
3+
import { FormsModule } from '@angular/forms';
34
import { Router } from '@angular/router';
4-
import { Observable } from 'rxjs';
5+
import { BehaviorSubject, Observable } from 'rxjs';
56
import { AuthService } from '~/services/auth.service';
7+
import { ApiKeysService, type ApiKey } from '~/services/api-keys.service';
68
import type { ISelf } from '@openworkers/api-types';
79

810
@Component({
911
standalone: true,
10-
imports: [CommonModule],
11-
template: `
12-
<div class="page">
13-
<h3 class="title">Account</h3>
14-
15-
<div>
16-
@if (user$ | async; as user) {
17-
<div class="mt-4">
18-
<div class="text-gray-500">Username:</div>
19-
<div class="text-gray-900 dark:text-stone-200 ml-2">{{ user.username }}</div>
20-
</div>
21-
<div class="mt-4">
22-
<div class="text-gray-500">Resources limits:</div>
23-
<ul class="text-gray-900 dark:text-stone-200 ml-2">
24-
<li>Workers: {{ user.limits.workers }}</li>
25-
<li>Environments: {{ user.limits.environments }}</li>
26-
<li>Databases: {{ user.limits.databases }}</li>
27-
<li>KV Namespaces: {{ user.limits.kv }}</li>
28-
</ul>
29-
</div>
30-
}
31-
</div>
32-
33-
<div class="mt-8">
34-
<button class="btn-outline" (click)="logout()">Logout</button>
35-
</div>
36-
</div>
37-
`
12+
imports: [CommonModule, FormsModule],
13+
templateUrl: './account.page.html'
3814
})
39-
export default class AccountPage {
15+
export default class AccountPage implements OnInit {
4016
user$: Observable<ISelf | null>;
17+
keys$: Observable<ApiKey[]>;
18+
creating$ = new BehaviorSubject<boolean>(false);
19+
20+
showCreateKey = false;
21+
newKeyName = '';
22+
newKeyToken: string | null = null;
4123

4224
constructor(
4325
private router: Router,
44-
private auth: AuthService
26+
private auth: AuthService,
27+
private apiKeys: ApiKeysService
4528
) {
4629
this.user$ = auth.user$;
30+
this.keys$ = apiKeys.keys$;
31+
}
32+
33+
ngOnInit() {
34+
this.apiKeys.loadKeys().subscribe();
35+
}
36+
37+
createKey() {
38+
if (!this.newKeyName) return;
39+
40+
this.creating$.next(true);
41+
this.apiKeys.createKey(this.newKeyName).subscribe({
42+
next: (key) => {
43+
this.newKeyToken = key.token;
44+
this.showCreateKey = false;
45+
this.newKeyName = '';
46+
this.creating$.next(false);
47+
},
48+
error: () => {
49+
this.creating$.next(false);
50+
}
51+
});
52+
}
53+
54+
cancelCreate() {
55+
this.showCreateKey = false;
56+
this.newKeyName = '';
57+
}
58+
59+
copyToken() {
60+
if (this.newKeyToken) {
61+
navigator.clipboard.writeText(this.newKeyToken);
62+
}
63+
}
64+
65+
deleteKey(id: string) {
66+
if (confirm('Are you sure you want to delete this API key?')) {
67+
this.apiKeys.deleteKey(id).subscribe();
68+
}
4769
}
4870

4971
logout() {

0 commit comments

Comments
 (0)