Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ This application uses Cloudflare Developer Platform, including Workers and Durab

- All non-admin API paths start with `/${usersPath}/api/`
- All admin API paths start with `/${usersPath}/admin/api/`

## Implementation

- When coding new features, create tests that cover the new code
30 changes: 22 additions & 8 deletions public/users/accounts.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
<title>Account Settings</title>
<link rel="stylesheet" href="/users/style.css" />
</head>
<body data-ssr-account="{{ssr:account_json}}" data-ssr-profile="{{ssr:profile_json}}" data-ssr-members="{{ssr:account_members_json}}">
<body
data-ssr-account="{{ssr:account_json}}"
data-ssr-profile="{{ssr:profile_json}}"
data-ssr-members="{{ssr:account_members_json}}"
data-ssr-plans="{{ssr:plans_json}}"
>
<power-strip
providers="{{ssr:providers}}"
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
Expand Down Expand Up @@ -316,7 +321,7 @@ <h2>Team Members</h2>
async function loadAccountDetails(ssrData) {
try {
let data = ssrData;
if (!data) {
if (!data || !data.billing) {
const res = await fetch(`${API_BASE}/me/accounts/${currentAccountId}`);
if (res.ok) {
data = await res.json();
Expand All @@ -329,14 +334,23 @@ <h2>Team Members</h2>
document.getElementById('display-account-name').textContent = data.name;
initialAccountInfo.name = data.name;
}
if (data.billing && data.billing.plan_details) {
document.getElementById('account-plan-display').value = data.billing.plan_details.name;
document.getElementById('display-account-plan').textContent = data.billing.plan_details.name;
} else if (data.billing && data.billing.state) {
document.getElementById('account-plan-display').value = data.billing.state.plan_slug;
document.getElementById('display-account-plan').textContent = data.billing.state.plan_slug;

// Get plan name from available plans
let availablePlans = [];
const ssrPlans = document.body.getAttribute('data-ssr-plans');
if (ssrPlans && !ssrPlans.startsWith('{{ssr:')) {
try {
availablePlans = JSON.parse(ssrPlans);
} catch (e) {}
}

const planSlug = data.billing?.state?.plan_slug || data.plan;
const plan = availablePlans.find((p) => p.slug === planSlug);
const planName = plan ? plan.name : planSlug || 'Free';

document.getElementById('account-plan-display').value = planName;
document.getElementById('display-account-plan').textContent = planName;

// Load avatar
const avatarRes = await fetch(`${API_BASE}/me/accounts/${currentAccountId}/avatar`);
if (avatarRes.ok) {
Expand Down
50 changes: 36 additions & 14 deletions public/users/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
}
</style>
</head>
<body>
<body data-ssr-plans="{{ssr:plans_json}}">
<power-strip
providers="{{ssr:providers}}"
style="position: absolute; top: 0; right: 0; z-index: 9999; padding: 0.1rem; border-radius: 0 0 0 0.3rem"
Expand Down Expand Up @@ -254,9 +254,7 @@ <h3>Edit Account</h3>
<div class="form-group">
<label for="edit-account-plan">Plan</label>
<select id="edit-account-plan">
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="enterprise">Enterprise</option>
<!-- Populated dynamically -->
</select>
</div>
<div class="modal-actions">
Expand All @@ -282,9 +280,7 @@ <h3>Create New Account</h3>
<div class="form-group">
<label for="create-account-plan">Initial Plan</label>
<select id="create-account-plan">
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="enterprise">Enterprise</option>
<!-- Populated dynamically -->
</select>
</div>
<div class="modal-actions">
Expand Down Expand Up @@ -336,6 +332,29 @@ <h4>Current Members</h4>
const API_BASE = '/users/admin/api';
let currentUsers = [];
let currentAccounts = [];
let availablePlans = [];

function getPlans() {
if (availablePlans.length > 0) return availablePlans;
const ssrPlans = document.body.getAttribute('data-ssr-plans');
if (ssrPlans && !ssrPlans.startsWith('{{ssr:')) {
try {
availablePlans = JSON.parse(ssrPlans);
return availablePlans;
} catch (e) {
console.error('Failed to parse SSR plans', e);
}
}
return [];
}

function populatePlanSelect(selectId, selectedValue) {
const select = document.getElementById(selectId);
const plans = getPlans();
select.innerHTML = plans
.map((p) => `<option value="${p.slug}" ${p.slug === selectedValue ? 'selected' : ''}>${p.name}</option>`)
.join('');
}

async function fetchAPI(endpoint, options = {}) {
try {
Expand Down Expand Up @@ -401,14 +420,17 @@ <h4>Current Members</h4>

function renderAccounts(accounts) {
const tbody = document.getElementById('accounts-table');
const plans = getPlans();
tbody.innerHTML = accounts
.map(
(a) => `
.map((a) => {
const plan = plans.find((p) => p.slug === a.plan);
const planName = plan ? plan.name : a.plan;
return `
<tr>
<td>${a.id.substring(0, 8)}...</td>
<td>${a.name || '-'}</td>
<td>${a.status || '-'}</td>
<td>${a.plan || '-'}</td>
<td>${planName || '-'}</td>
<td>${a.member_count || 0}</td>
<td>${new Date(a.created_at).toLocaleDateString()}</td>
<td class="actions">
Expand All @@ -417,8 +439,8 @@ <h4>Current Members</h4>
<button style="background: #d93025;" onclick="deleteAccount('${a.id}')">Delete</button>
</td>
</tr>
`,
)
`;
})
.join('');
}

Expand Down Expand Up @@ -453,7 +475,7 @@ <h4>Current Members</h4>
document.getElementById('edit-account-id').value = account.id;
document.getElementById('edit-account-name').value = account.name || '';
document.getElementById('edit-account-status').value = account.status || 'active';
document.getElementById('edit-account-plan').value = account.plan || 'free';
populatePlanSelect('edit-account-plan', account.plan || 'free');
document.getElementById('edit-account-modal').showModal();
}

Expand All @@ -469,7 +491,7 @@ <h4>Current Members</h4>
});

document.getElementById('create-account-name').value = '';
document.getElementById('create-account-plan').value = 'free';
populatePlanSelect('create-account-plan', 'free');
document.getElementById('create-account-modal').showModal();
}

Expand Down
81 changes: 58 additions & 23 deletions src/AccountDO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ export class AccountDO extends DurableObject {
// Initialize database schema
this.sql.exec(`
CREATE TABLE IF NOT EXISTS account_info (
key TEXT PRIMARY KEY,
value TEXT
id INTEGER PRIMARY KEY CHECK (id = 1),
name TEXT,
plan TEXT,
billing TEXT,
personal INTEGER
);

CREATE TABLE IF NOT EXISTS members (
Expand All @@ -36,6 +39,9 @@ export class AccountDO extends DurableObject {
joined_at INTEGER
);
`);

// Ensure the single row exists
this.sql.exec('INSERT OR IGNORE INTO account_info (id, plan) VALUES (1, "free")');
}

async getImage(key: string) {
Expand Down Expand Up @@ -63,28 +69,57 @@ export class AccountDO extends DurableObject {
}

async getInfo() {
const info: Record<string, any> = {};
try {
const result = this.sql.exec('SELECT key, value FROM account_info');
for (const row of result) {
// @ts-ignore
info[row.key] = JSON.parse(row.value as string);
}
} catch (e) {}
return info;
const result = this.sql.exec('SELECT * FROM account_info WHERE id = 1');
const row = result.next().value as any;
if (!row) return {};

return {
name: row.name,
plan: row.plan,
personal: row.personal === 1,
billing: row.billing ? JSON.parse(row.billing) : undefined,
};
} catch (e) {
return {};
}
}

async updateInfo(data: Record<string, any>) {
try {
this.ctx.storage.transactionSync(() => {
for (const [key, value] of Object.entries(data)) {
let valToStore = value;
if (key === 'name' && typeof value === 'string') {
valToStore = value.substring(0, 50);
const updates: string[] = [];
const values: any[] = [];

if ('name' in data) {
updates.push('name = ?');
values.push(typeof data.name === 'string' ? data.name.substring(0, 50) : data.name);
}
if ('plan' in data) {
updates.push('plan = ?');
values.push(data.plan);
}
if ('personal' in data) {
updates.push('personal = ?');
values.push(data.personal ? 1 : 0);
}

if (updates.length > 0) {
this.ctx.storage.transactionSync(() => {
this.sql.exec(`UPDATE account_info SET ${updates.join(', ')} WHERE id = 1`, ...values);

// If plan is updated manually (e.g. via Admin API), update billing state too
if ('plan' in data) {
const currentState = this.getBillingState();
if (currentState.plan_slug !== data.plan) {
const newState = {
...currentState,
plan_slug: data.plan,
};
this.sql.exec('UPDATE account_info SET billing = ? WHERE id = 1', JSON.stringify(newState));
}
}
this.sql.exec('INSERT OR REPLACE INTO account_info (key, value) VALUES (?, ?)', key, JSON.stringify(valToStore));
}
});
});
}
return { success: true };
} catch (e: any) {
throw new Error(e.message);
Expand Down Expand Up @@ -211,10 +246,10 @@ export class AccountDO extends DurableObject {

private getBillingState(): any {
try {
const result = this.sql.exec("SELECT value FROM account_info WHERE key = 'billing'");
for (const row of result) {
// @ts-ignore
return JSON.parse(row.value as string);
const result = this.sql.exec('SELECT billing FROM account_info WHERE id = 1');
const row = result.next().value as any;
if (row && row.billing) {
return JSON.parse(row.billing);
}
} catch (e) {}
return {
Expand All @@ -225,7 +260,7 @@ export class AccountDO extends DurableObject {

private setBillingState(state: any) {
this.ctx.storage.transactionSync(() => {
this.sql.exec("INSERT OR REPLACE INTO account_info (key, value) VALUES ('billing', ?)", JSON.stringify(state));
this.sql.exec('UPDATE account_info SET billing = ?, plan = ? WHERE id = 1', JSON.stringify(state), state.plan_slug);
});
}

Expand Down
Loading