Almost complete org/permission handling. Much cleanup, bootstrap works.

This commit is contained in:
Leo Vasanko 2025-08-07 13:58:12 -06:00
parent 2e4ff30bea
commit 407994548a
12 changed files with 225 additions and 341 deletions

View File

@ -2,7 +2,6 @@
<div>
<StatusMessage />
<LoginView v-if="store.currentView === 'login'" />
<RegisterView v-if="store.currentView === 'register'" />
<ProfileView v-if="store.currentView === 'profile'" />
<DeviceLinkView v-if="store.currentView === 'device-link'" />
<ResetView v-if="store.currentView === 'reset'" />
@ -14,7 +13,6 @@ import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import StatusMessage from '@/components/StatusMessage.vue'
import LoginView from '@/components/LoginView.vue'
import RegisterView from '@/components/RegisterView.vue'
import ProfileView from '@/components/ProfileView.vue'
import DeviceLinkView from '@/components/DeviceLinkView.vue'
import ResetView from '@/components/ResetView.vue'

View File

@ -11,11 +11,6 @@
{{ authStore.isLoading ? 'Authenticating...' : 'Login with Your Device' }}
</button>
</form>
<p class="toggle-link">
<a href="#" @click.prevent="authStore.currentView = 'register'">
Don't have an account? Register here
</a>
</p>
</div>
</div>
</template>

View File

@ -81,7 +81,7 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { formatDate } from '@/utils/helpers'
import { registerCredential } from '@/utils/passkey'
import passkey from '@/utils/passkey'
const authStore = useAuthStore()
const updateInterval = ref(null)
@ -119,7 +119,7 @@ const addNewCredential = async () => {
try {
authStore.isLoading = true
authStore.showMessage('Adding new passkey...', 'info')
const result = await registerCredential()
await passkey.register()
await authStore.loadUserInfo()
authStore.showMessage('New passkey added successfully!', 'success', 3000)
} catch (error) {

View File

@ -1,57 +0,0 @@
<template>
<div class="container">
<div class="view active">
<h1>🔐 Create Account</h1>
<form @submit.prevent="handleRegister">
<input
type="text"
v-model="user_name"
placeholder="Enter username"
required
:disabled="authStore.isLoading"
>
<button
type="submit"
class="btn-primary"
:disabled="authStore.isLoading || !user_name.trim()"
>
{{ authStore.isLoading ? 'Registering...' : 'Register Passkey' }}
</button>
</form>
<p class="toggle-link">
<a href="#" @click.prevent="authStore.currentView = 'login'">
Already have an account? Login here
</a>
</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const user_name = ref('')
const handleRegister = async () => {
if (!user_name.value.trim()) return
try {
authStore.showMessage('Starting registration...', 'info')
await authStore.register(user_name.value.trim())
authStore.showMessage('Passkey registered successfully!', 'success', 2000)
setTimeout(() => {
authStore.currentView = 'profile'
}, 1500)
} catch (error) {
console.error('Registration error:', error)
if (error.name === "NotAllowedError") {
authStore.showMessage('Registration cancelled', 'error')
} else {
authStore.showMessage(`Registration failed: ${error.message}`, 'error')
}
}
}
</script>

View File

@ -3,6 +3,7 @@
<div class="view active">
<h1>🔑 Add New Credential</h1>
<h3>👤 {{ authStore.userInfo?.user?.user_name }}</h3>
<!-- TODO: allow editing name <input type="text" v-model="user_name" required :disabled="authStore.isLoading"> -->
<p>Proceed to complete {{authStore.userInfo?.session_type}}:</p>
<button
class="btn-primary"
@ -17,7 +18,7 @@
<script setup>
import { useAuthStore } from '@/stores/auth'
import { registerCredential } from '@/utils/passkey'
import passkey from '@/utils/passkey'
const authStore = useAuthStore()
@ -26,8 +27,7 @@ async function register() {
authStore.showMessage('Starting registration...', 'info')
try {
// TODO: For reset sessions, might use registerWithToken() in the future
const result = await registerCredential()
const result = await passkey.register()
console.log("Result", result)
await authStore.setSessionCookie(result.session_token)

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { registerUser, authenticateUser, registerWithToken } from '@/utils/passkey'
import { register, authenticate } from '@/utils/passkey'
export const useAuthStore = defineStore('auth', {
state: () => ({
@ -8,7 +8,7 @@ export const useAuthStore = defineStore('auth', {
isLoading: false,
// UI State
currentView: 'login', // 'login', 'register', 'profile', 'reset'
currentView: 'login', // 'login', 'profile', 'device-link', 'reset'
status: {
message: '',
type: 'info',
@ -39,23 +39,12 @@ export const useAuthStore = defineStore('auth', {
}
return result
},
async register(user_name) {
async register() {
this.isLoading = true
try {
const result = await registerUser(user_name)
this.userInfo = {
user: {
user_id: result.user_id,
user_name: user_name,
},
credentials: [],
aaguid_info: {},
session_type: null,
authenticated: false
}
const result = await register()
await this.setSessionCookie(result.session_token)
await this.loadUserInfo()
return result
} finally {
this.isLoading = false
@ -64,7 +53,7 @@ export const useAuthStore = defineStore('auth', {
async authenticate() {
this.isLoading = true
try {
const result = await authenticateUser()
const result = await authenticate()
await this.setSessionCookie(result.session_token)
await this.loadUserInfo()

View File

@ -1,14 +1,13 @@
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
import aWebSocket from '@/utils/awaitable-websocket'
export async function register(url, options) {
if (options) url += `?${new URLSearchParams(options).toString()}`
const ws = await aWebSocket(url)
export async function register() {
const ws = await aWebSocket("/auth/ws/register")
try {
const optionsJSON = await ws.receive_json()
const registrationResponse = await startRegistration({ optionsJSON })
ws.send_json(registrationResponse)
const result = await ws.receive_json()
return await ws.receive_json()
} catch (error) {
console.error('Registration error:', error)
// Replace useless and ugly error message from startRegistration
@ -18,18 +17,7 @@ export async function register(url, options) {
}
}
export async function registerUser(user_name) {
return register('/auth/ws/register', { user_name })
}
export async function registerCredential() {
return register('/auth/ws/add_credential')
}
export async function registerWithToken(token) {
return register('/auth/ws/add_credential', { token })
}
export async function authenticateUser() {
export async function authenticate() {
const ws = await aWebSocket('/auth/ws/authenticate')
try {
const optionsJSON = await ws.receive_json()
@ -44,3 +32,5 @@ export async function authenticateUser() {
ws.close()
}
}
export default { authenticate, register }

View File

@ -13,7 +13,7 @@ from datetime import datetime
import uuid7
from . import authsession, globals
from .db import Org, Permission, User
from .db import Org, Permission, Role, User
from .util import passphrase, tokens
logger = logging.getLogger(__name__)
@ -54,62 +54,42 @@ async def bootstrap_system(
dict: Contains information about created entities and reset link
"""
# Create permission first - will fail if already exists
permission = Permission(id="auth/admin", display_name="Admin")
await globals.db.instance.create_permission(permission)
perm0 = Permission(id="auth/admin", display_name="Master Admin")
await globals.db.instance.create_permission(perm0)
# Create organization
org_uuid = uuid7.create()
org = Org(
id=str(org_uuid),
options={
"display_name": org_name or "Organization",
"created_at": datetime.now().isoformat(),
},
)
org = Org(uuid7.create(), org_name or "Organization")
await globals.db.instance.create_organization(org)
# Create admin user
admin_uuid = uuid7.create()
perm1 = Permission(
id=f"auth/org:{org.uuid}", display_name=f"{org.display_name} Admin"
)
await globals.db.instance.create_permission(perm1)
role = Role(uuid7.create(), org.uuid, "Administration")
await globals.db.instance.create_role(role)
user = User(
uuid=admin_uuid,
uuid=uuid7.create(),
display_name=user_name or "Admin",
org_uuid=org_uuid,
role="Admin",
role_uuid=role.uuid,
created_at=datetime.now(),
visits=0,
)
await globals.db.instance.create_user(user)
# Link user to organization and assign permissions
await globals.db.instance.add_user_to_organization(
user_uuid=admin_uuid, org_id=org.id, role="Admin"
)
await globals.db.instance.add_permission_to_organization(org.id, permission.id)
# Generate reset link and log it
reset_link = await _create_and_log_admin_reset_link(
admin_uuid, "✅ Bootstrap completed!", "admin bootstrap"
user.uuid, "✅ Bootstrap completed!", "admin bootstrap"
)
result = {
"admin_user": {
"uuid": str(user.uuid),
"display_name": user.display_name,
"role": user.role,
},
"organization": {
"id": org.id,
"display_name": org.options.get("display_name"),
},
"permission": {
"id": permission.id,
"display_name": permission.display_name,
},
return {
"user": user,
"org": org,
"role": role,
"permissions": [perm0, perm1],
"reset_link": reset_link,
}
return result
async def check_admin_credentials() -> bool:
"""
@ -175,14 +155,7 @@ async def bootstrap_if_needed(
await globals.db.instance.get_permission("auth/admin")
# Permission exists, system is already bootstrapped
# Check if admin needs credentials (only for already-bootstrapped systems)
admin_needs_reset = await check_admin_credentials()
if not admin_needs_reset:
# Use the same format as the reset link messages
logger.info(
ADMIN_RESET_MESSAGE,
" System already bootstrapped - no action needed",
"Admin user already has credentials",
)
await check_admin_credentials()
return False
except Exception:
# Permission doesn't exist, need to bootstrap

View File

@ -11,12 +11,24 @@ from datetime import datetime
from uuid import UUID
@dataclass
class Org:
uuid: UUID
display_name: str
@dataclass
class Role:
uuid: UUID
org_uuid: UUID
display_name: str
@dataclass
class User:
uuid: UUID
display_name: str
org_uuid: UUID
role: str | None = None
role_uuid: UUID
created_at: datetime | None = None
last_seen: datetime | None = None
visits: int = 0
@ -35,26 +47,8 @@ class Credential:
last_verified: datetime | None = None
@dataclass
class Org:
"""Organization data structure."""
id: str # ASCII primary key
options: dict
@dataclass
class Permission:
"""Permission data structure."""
id: str # String primary key (max 32 chars)
display_name: str
@dataclass
class Session:
"""Session data structure."""
key: bytes
user_uuid: UUID
expires: datetime
@ -63,13 +57,17 @@ class Session:
@dataclass
class SessionContext:
"""Complete session context with user, organization, role, and permissions."""
class Permission:
id: str # String primary key (max 128 chars)
display_name: str
@dataclass
class SessionContext:
session: Session
user: User
organization: Org
role: str | None = None
org: Org
role: Role
permissions: list[Permission] | None = None
@ -96,6 +94,11 @@ class DatabaseInterface(ABC):
async def create_user(self, user: User) -> None:
"""Create a new user."""
# Role operations
@abstractmethod
async def create_role(self, role: Role) -> None:
"""Create new role."""
# Credential operations
@abstractmethod
async def create_credential(self, credential: Credential) -> None:
@ -149,19 +152,19 @@ class DatabaseInterface(ABC):
# Organization operations
@abstractmethod
async def create_organization(self, organization: Org) -> None:
"""Create a new organization."""
async def create_organization(self, org: Org) -> None:
"""Add a new organization."""
@abstractmethod
async def get_organization(self, org_id: str) -> Org:
"""Get organization by ID."""
@abstractmethod
async def update_organization(self, organization: Org) -> None:
async def update_organization(self, org: Org) -> None:
"""Update organization options."""
@abstractmethod
async def delete_organization(self, org_id: str) -> None:
async def delete_organization(self, org_uuid: UUID) -> None:
"""Delete organization by ID."""
@abstractmethod

View File

@ -29,6 +29,7 @@ from . import (
DatabaseInterface,
Org,
Permission,
Role,
Session,
SessionContext,
User,
@ -42,41 +43,47 @@ async def init(*args, **kwargs):
await db.instance.init_db()
# SQLAlchemy Models
class Base(DeclarativeBase):
pass
# Association model for many-to-many relationship between organizations and permissions
class OrgPermission(Base):
"""Permissions each Org is allowed to grant to its roles."""
__tablename__ = "org_permissions"
org_uuid: Mapped[bytes] = mapped_column(
LargeBinary(16),
ForeignKey("orgs.uuid", ondelete="CASCADE"),
primary_key=True,
)
permission_id: Mapped[str] = mapped_column(
String(32),
ForeignKey("permissions.id", ondelete="CASCADE"),
primary_key=True,
)
class PermissionModel(Base):
__tablename__ = "permissions"
id: Mapped[str] = mapped_column(String(128), primary_key=True)
display_name: Mapped[str] = mapped_column(String, nullable=False)
class OrgModel(Base):
__tablename__ = "orgs"
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
options: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict)
display_name: Mapped[str] = mapped_column(String, nullable=False)
def as_dataclass(self):
return Org(UUID(bytes=self.uuid), self.display_name)
@staticmethod
def from_dataclass(org: Org):
return OrgModel(uuid=org.uuid.bytes, display_name=org.display_name)
class RoleModel(Base):
__tablename__ = "roles"
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
org_uuid: Mapped[bytes] = mapped_column(
LargeBinary(16), ForeignKey("orgs.uuid", ondelete="CASCADE"), nullable=False
)
display_name: Mapped[str] = mapped_column(String, nullable=False)
def as_dataclass(self):
return Role(
uuid=UUID(bytes=self.uuid),
org_uuid=UUID(bytes=self.org_uuid),
display_name=self.display_name,
)
@staticmethod
def from_dataclass(role: Role):
return RoleModel(
uuid=role.uuid.bytes,
org_uuid=role.org_uuid.bytes,
display_name=role.display_name,
)
class UserModel(Base):
@ -84,17 +91,38 @@ class UserModel(Base):
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
display_name: Mapped[str] = mapped_column(String, nullable=False)
org_uuid: Mapped[bytes] = mapped_column(
LargeBinary(16), ForeignKey("orgs.uuid", ondelete="CASCADE"), nullable=False
role_uuid: Mapped[bytes] = mapped_column(
LargeBinary(16), ForeignKey("roles.uuid", ondelete="CASCADE"), nullable=False
)
role: Mapped[str | None] = mapped_column(String, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
last_seen: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
def as_dataclass(self) -> User:
return User(
uuid=UUID(bytes=self.uuid),
display_name=self.display_name,
role_uuid=UUID(bytes=self.role_uuid),
created_at=self.created_at,
last_seen=self.last_seen,
visits=self.visits,
)
@staticmethod
def from_dataclass(user: User):
return UserModel(
uuid=user.uuid.bytes,
display_name=user.display_name,
role_uuid=user.role_uuid.bytes,
created_at=user.created_at or datetime.now(),
last_seen=user.last_seen,
visits=user.visits,
)
class CredentialModel(Base):
__tablename__ = "credentials"
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
credential_id: Mapped[bytes] = mapped_column(
LargeBinary(64), unique=True, index=True
@ -121,7 +149,73 @@ class SessionModel(Base):
LargeBinary(16), ForeignKey("credentials.uuid", ondelete="CASCADE")
)
expires: Mapped[datetime] = mapped_column(DateTime, nullable=False)
info: Mapped[dict | None] = mapped_column(JSON, nullable=True)
info: Mapped[dict] = mapped_column(JSON, default=dict)
def as_dataclass(self):
return Session(
key=self.key,
user_uuid=UUID(bytes=self.user_uuid),
credential_uuid=(
UUID(bytes=self.credential_uuid) if self.credential_uuid else None
),
expires=self.expires,
info=self.info,
)
@staticmethod
def from_dataclass(session: Session):
return SessionModel(
key=session.key,
user_uuid=session.user_uuid.bytes,
credential_uuid=session.credential_uuid and session.credential_uuid.bytes,
expires=session.expires,
info=session.info,
)
class PermissionModel(Base):
__tablename__ = "permissions"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
display_name: Mapped[str] = mapped_column(String, nullable=False)
def as_dataclass(self):
return Permission(self.id, self.display_name)
@staticmethod
def from_dataclass(permission: Permission):
return PermissionModel(id=permission.id, display_name=permission.display_name)
## Join tables (no dataclass equivalents)
class OrgPermission(Base):
"""Permissions each organization is allowed to grant to its roles."""
__tablename__ = "org_permissions"
id: Mapped[int] = mapped_column(Integer, primary_key=True) # Not used
org_uuid: Mapped[bytes] = mapped_column(
LargeBinary(16), ForeignKey("orgs.uuid", ondelete="CASCADE")
)
permission_id: Mapped[str] = mapped_column(
String(64), ForeignKey("permissions.id", ondelete="CASCADE")
)
class RolePermission(Base):
"""Permissions that each role grants to its members."""
__tablename__ = "role_permissions"
id: Mapped[int] = mapped_column(Integer, primary_key=True) # Not used
role_uuid: Mapped[bytes] = mapped_column(
LargeBinary(16), ForeignKey("roles.uuid", ondelete="CASCADE")
)
permission_id: Mapped[str] = mapped_column(
String(64), ForeignKey("permissions.id", ondelete="CASCADE")
)
class DB(DatabaseInterface):
@ -155,29 +249,16 @@ class DB(DatabaseInterface):
user_model = result.scalar_one_or_none()
if user_model:
return User(
uuid=UUID(bytes=user_model.uuid),
display_name=user_model.display_name,
org_uuid=UUID(bytes=user_model.org_uuid),
role=user_model.role,
created_at=user_model.created_at,
last_seen=user_model.last_seen,
visits=user_model.visits,
)
return user_model.as_dataclass()
raise ValueError("User not found")
async def create_user(self, user: User) -> None:
async with self.session() as session:
user_model = UserModel(
uuid=user.uuid.bytes,
display_name=user.display_name,
org_uuid=user.org_uuid.bytes,
role=user.role,
created_at=user.created_at or datetime.now(),
last_seen=user.last_seen,
visits=user.visits,
)
session.add(user_model)
session.add(UserModel.from_dataclass(user))
async def create_role(self, role: Role) -> None:
async with self.session() as session:
session.add(RoleModel.from_dataclass(role))
async def create_credential(self, credential: Credential) -> None:
async with self.session() as session:
@ -265,19 +346,8 @@ class DB(DatabaseInterface):
self, user: User, credential: Credential
) -> None:
async with self.session() as session:
# Set visits to 1 for the new user since they're creating their first session
user.visits = 1
# Create user
user_model = UserModel(
uuid=user.uuid.bytes,
display_name=user.display_name,
org_uuid=user.org_uuid.bytes,
role=user.role,
created_at=user.created_at or datetime.now(),
last_seen=user.last_seen,
visits=user.visits,
)
user_model = UserModel.from_dataclass(user)
session.add(user_model)
# Create credential
@ -352,13 +422,11 @@ class DB(DatabaseInterface):
)
# Organization operations
async def create_organization(self, organization: Org) -> None:
async def create_organization(self, org: Org) -> None:
async with self.session() as session:
# Convert string ID to UUID bytes for storage
org_uuid = UUID(organization.id)
org_model = OrgModel(
uuid=org_uuid.bytes,
options=organization.options,
uuid=org.uuid.bytes,
display_name=org.display_name,
)
session.add(org_model)
@ -370,34 +438,28 @@ class DB(DatabaseInterface):
result = await session.execute(stmt)
org_model = result.scalar_one_or_none()
if org_model:
# Convert UUID bytes back to string for the interface
return Org(
id=str(UUID(bytes=org_model.uuid)),
options=org_model.options,
)
raise ValueError("Organization not found")
if not org_model:
raise ValueError("Organization not found")
async def update_organization(self, organization: Org) -> None:
return org_model.as_dataclass()
async def update_organization(self, org: Org) -> None:
async with self.session() as session:
# Convert string ID to UUID bytes for lookup
org_uuid = UUID(organization.id)
stmt = (
update(OrgModel)
.where(OrgModel.uuid == org_uuid.bytes)
.values(options=organization.options)
.where(OrgModel.uuid == org.uuid.bytes)
.values(display_name=org.display_name)
)
await session.execute(stmt)
async def delete_organization(self, org_id: str) -> None:
async def delete_organization(self, org_uuid: UUID) -> None:
async with self.session() as session:
# Convert string ID to UUID bytes for lookup
org_uuid = UUID(org_id)
stmt = delete(OrgModel).where(OrgModel.uuid == org_uuid.bytes)
await session.execute(stmt)
async def add_user_to_organization(
self, user_uuid: UUID, org_id: str, role: str
self, user_uuid: UUID, org_uuid: UUID, role: str
) -> None:
async with self.session() as session:
# Get user and organization models
@ -406,7 +468,6 @@ class DB(DatabaseInterface):
user_model = user_result.scalar_one_or_none()
# Convert string ID to UUID bytes for lookup
org_uuid = UUID(org_id)
org_stmt = select(OrgModel).where(OrgModel.uuid == org_uuid.bytes)
org_result = await session.execute(org_stmt)
org_model = org_result.scalar_one_or_none()
@ -449,7 +510,7 @@ class DB(DatabaseInterface):
if result.rowcount == 0:
raise ValueError("User not found")
async def get_user_organization(self, user_uuid: UUID) -> tuple[Org, str]:
async def get_user_organization(self, user_uuid: UUID) -> Org:
async with self.session() as session:
stmt = select(UserModel).where(UserModel.uuid == user_uuid.bytes)
result = await session.execute(stmt)
@ -459,37 +520,19 @@ class DB(DatabaseInterface):
raise ValueError("User not found")
# Fetch the organization details
org_stmt = select(OrgModel).where(OrgModel.uuid == user_model.org_uuid)
org_stmt = select(OrgModel).where(OrgModel.uuid == user_model.role_uuid)
org_result = await session.execute(org_stmt)
org_model = org_result.scalar_one()
# Convert UUID bytes back to string for the interface
org = Org(id=str(UUID(bytes=org_model.uuid)), options=org_model.options)
return (org, user_model.role or "")
return org_model.as_dataclass()
async def get_organization_users(self, org_id: str) -> list[tuple[User, str]]:
async def get_organization_users(self, org_id: str) -> list[User]:
async with self.session() as session:
# Convert string ID to UUID bytes for lookup
org_uuid = UUID(org_id)
stmt = select(UserModel).where(UserModel.org_uuid == org_uuid.bytes)
stmt = select(UserModel).where(UserModel.role_uuid == role.uuid.bytes)
result = await session.execute(stmt)
user_models = result.scalars().all()
# Create user objects with their roles
user_role_pairs = []
for user_model in user_models:
user = User(
uuid=UUID(bytes=user_model.uuid),
display_name=user_model.display_name,
org_uuid=UUID(bytes=user_model.org_uuid),
role=user_model.role,
created_at=user_model.created_at,
last_seen=user_model.last_seen,
visits=user_model.visits,
)
user_role_pairs.append((user, user_model.role or ""))
return user_role_pairs
return [u.as_dataclass() for u in user_models]
async def get_user_role_in_organization(
self, user_uuid: UUID, org_id: str
@ -638,8 +681,7 @@ class DB(DatabaseInterface):
org_model = org_result.scalar_one()
# Convert UUID bytes back to string for the interface
org = Org(id=str(UUID(bytes=org_model.uuid)), options=org_model.options)
organizations.append(org)
organizations.append(org.as_dataclass())
return organizations
@ -695,21 +737,10 @@ class DB(DatabaseInterface):
)
# Create the user object
user_obj = User(
uuid=UUID(bytes=user_model.uuid),
display_name=user_model.display_name,
org_uuid=UUID(bytes=user_model.org_uuid),
role=user_model.role,
created_at=user_model.created_at,
last_seen=user_model.last_seen,
visits=user_model.visits,
)
user_obj = user_model.as_dataclass()
# Create organization object (always exists now)
organization = Org(
id=str(UUID(bytes=org_model.uuid)),
options=org_model.options,
)
organization = Org(UUID(bytes=org_model.uuid), org_model.display_name)
# Collect all unique permissions
permissions = []
@ -728,7 +759,7 @@ class DB(DatabaseInterface):
return SessionContext(
session=session_obj,
user=user_obj,
organization=organization,
org=organization,
role=user_model.role,
permissions=permissions if permissions else None,
)

View File

@ -1,17 +1,13 @@
import logging
from datetime import datetime
from functools import wraps
from uuid import UUID
import uuid7
from fastapi import Cookie, FastAPI, Query, WebSocket, WebSocketDisconnect
from fastapi import Cookie, FastAPI, WebSocket, WebSocketDisconnect
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
from ..authsession import EXPIRES, create_session, get_reset, get_session
from ..db import User
from ..authsession import create_session, get_reset, get_session
from ..globals import db, passkey
from ..util import passphrase
from ..util.tokens import create_token, session_key
from .session import infodict
@ -58,40 +54,6 @@ async def register_chat(
@app.websocket("/register")
@websocket_error_handler
async def websocket_register_new(
ws: WebSocket, user_name: str = Query(""), auth=Cookie(None)
):
"""Register a new user and with a new passkey credential."""
origin = ws.headers["origin"]
user_uuid = uuid7.create()
# WebAuthn registration
credential = await register_chat(ws, user_uuid, user_name, origin=origin)
# Store the user and credential in the database
await db.instance.create_user_and_credential(
User(user_uuid, user_name, created_at=datetime.now()),
credential,
)
# Create a session token for the new user
token = create_token()
await db.instance.create_session(
user_uuid=user_uuid,
key=session_key(token),
expires=datetime.now() + EXPIRES,
info=infodict(ws, "authenticated"),
credential_uuid=credential.uuid,
)
await ws.send_json(
{
"user_uuid": str(user_uuid),
"session_token": token,
}
)
@app.websocket("/add_credential")
@websocket_error_handler
async def websocket_register_add(ws: WebSocket, auth=Cookie(None)):
"""Register a new credential for an existing user."""
origin = ws.headers["origin"]