feat(admin): allow setting owner when creating an account #140
@@ -431,10 +431,19 @@ async def create_account(
|
|||||||
current_user: Annotated[User, Depends(require_admin)],
|
current_user: Annotated[User, Depends(require_admin)],
|
||||||
):
|
):
|
||||||
"""Create a new account without requiring an initial user."""
|
"""Create a new account without requiring an initial user."""
|
||||||
|
owner_id = None
|
||||||
|
if data.owner_email:
|
||||||
|
result = await db.execute(select(User).where(User.email == data.owner_email.strip()))
|
||||||
|
owner = result.scalar_one_or_none()
|
||||||
|
if not owner:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"No user found with email '{data.owner_email}'")
|
||||||
|
owner_id = owner.id
|
||||||
|
|
||||||
display_code = await _generate_unique_display_code(db)
|
display_code = await _generate_unique_display_code(db)
|
||||||
new_account = Account(
|
new_account = Account(
|
||||||
name=data.name.strip(),
|
name=data.name.strip(),
|
||||||
display_code=display_code,
|
display_code=display_code,
|
||||||
|
owner_id=owner_id,
|
||||||
)
|
)
|
||||||
db.add(new_account)
|
db.add(new_account)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
@@ -448,7 +457,7 @@ async def create_account(
|
|||||||
|
|
||||||
await log_audit(
|
await log_audit(
|
||||||
db, current_user.id, "account.create_admin", "account", new_account.id,
|
db, current_user.id, "account.create_admin", "account", new_account.id,
|
||||||
{"name": new_account.name, "plan": data.plan},
|
{"name": new_account.name, "plan": data.plan, "owner_email": data.owner_email},
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return await _get_account_detail_payload(new_account.id, db)
|
return await _get_account_detail_payload(new_account.id, db)
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ class AdminAccountDetailResponse(AdminAccountListItem):
|
|||||||
class AdminAccountCreate(BaseModel):
|
class AdminAccountCreate(BaseModel):
|
||||||
name: str = Field(..., min_length=1, max_length=255)
|
name: str = Field(..., min_length=1, max_length=255)
|
||||||
plan: Literal["free", "pro", "team"] = "free"
|
plan: Literal["free", "pro", "team"] = "free"
|
||||||
|
owner_email: Optional[EmailStr] = Field(None, description="Email of an existing user to set as owner")
|
||||||
|
|
||||||
|
|
||||||
class AdminAccountUpdate(BaseModel):
|
class AdminAccountUpdate(BaseModel):
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function UsersPage() {
|
|||||||
})
|
})
|
||||||
const [inviteLoading, setInviteLoading] = useState(false)
|
const [inviteLoading, setInviteLoading] = useState(false)
|
||||||
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
const [showCreateAccountModal, setShowCreateAccountModal] = useState(false)
|
||||||
const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'team' })
|
const [createAccountForm, setCreateAccountForm] = useState({ name: '', plan: 'free' as 'free' | 'pro' | 'team', owner_email: '' })
|
||||||
const [createAccountLoading, setCreateAccountLoading] = useState(false)
|
const [createAccountLoading, setCreateAccountLoading] = useState(false)
|
||||||
|
|
||||||
const fetchAccounts = useCallback(async () => {
|
const fetchAccounts = useCallback(async () => {
|
||||||
@@ -223,13 +223,19 @@ export function UsersPage() {
|
|||||||
const created = await adminApi.createAccount({
|
const created = await adminApi.createAccount({
|
||||||
name: createAccountForm.name.trim(),
|
name: createAccountForm.name.trim(),
|
||||||
plan: createAccountForm.plan,
|
plan: createAccountForm.plan,
|
||||||
|
owner_email: createAccountForm.owner_email.trim() || undefined,
|
||||||
})
|
})
|
||||||
toast.success('Account created')
|
toast.success('Account created')
|
||||||
setShowCreateAccountModal(false)
|
setShowCreateAccountModal(false)
|
||||||
setCreateAccountForm({ name: '', plan: 'free' })
|
setCreateAccountForm({ name: '', plan: 'free', owner_email: '' })
|
||||||
navigate(`/admin/accounts/${created.id}`)
|
navigate(`/admin/accounts/${created.id}`)
|
||||||
} catch {
|
} catch (err: unknown) {
|
||||||
toast.error('Failed to create account')
|
if (err && typeof err === 'object' && 'response' in err) {
|
||||||
|
const axiosErr = err as { response?: { data?: { detail?: string } } }
|
||||||
|
toast.error(axiosErr.response?.data?.detail || 'Failed to create account')
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to create account')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setCreateAccountLoading(false)
|
setCreateAccountLoading(false)
|
||||||
}
|
}
|
||||||
@@ -634,6 +640,18 @@ export function UsersPage() {
|
|||||||
<option value="team">Team</option>
|
<option value="team">Team</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-sm font-medium text-foreground">
|
||||||
|
Owner Email <span className="text-muted-foreground font-normal">(optional)</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={createAccountForm.owner_email}
|
||||||
|
onChange={(e) => setCreateAccountForm((form) => ({ ...form, owner_email: e.target.value }))}
|
||||||
|
placeholder="owner@example.com"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">Must be an existing user.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export interface AdminAccountDetailResponse extends AdminAccountListItem {
|
|||||||
export interface AdminAccountCreate {
|
export interface AdminAccountCreate {
|
||||||
name: string
|
name: string
|
||||||
plan: 'free' | 'pro' | 'team'
|
plan: 'free' | 'pro' | 'team'
|
||||||
|
owner_email?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminAccountUpdate {
|
export interface AdminAccountUpdate {
|
||||||
|
|||||||
Reference in New Issue
Block a user