Almost complete org/permission handling. Much cleanup, bootstrap works.
This commit is contained in:
parent
2e4ff30bea
commit
407994548a
@ -2,7 +2,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<StatusMessage />
|
<StatusMessage />
|
||||||
<LoginView v-if="store.currentView === 'login'" />
|
<LoginView v-if="store.currentView === 'login'" />
|
||||||
<RegisterView v-if="store.currentView === 'register'" />
|
|
||||||
<ProfileView v-if="store.currentView === 'profile'" />
|
<ProfileView v-if="store.currentView === 'profile'" />
|
||||||
<DeviceLinkView v-if="store.currentView === 'device-link'" />
|
<DeviceLinkView v-if="store.currentView === 'device-link'" />
|
||||||
<ResetView v-if="store.currentView === 'reset'" />
|
<ResetView v-if="store.currentView === 'reset'" />
|
||||||
@ -14,7 +13,6 @@ import { onMounted } from 'vue'
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import StatusMessage from '@/components/StatusMessage.vue'
|
import StatusMessage from '@/components/StatusMessage.vue'
|
||||||
import LoginView from '@/components/LoginView.vue'
|
import LoginView from '@/components/LoginView.vue'
|
||||||
import RegisterView from '@/components/RegisterView.vue'
|
|
||||||
import ProfileView from '@/components/ProfileView.vue'
|
import ProfileView from '@/components/ProfileView.vue'
|
||||||
import DeviceLinkView from '@/components/DeviceLinkView.vue'
|
import DeviceLinkView from '@/components/DeviceLinkView.vue'
|
||||||
import ResetView from '@/components/ResetView.vue'
|
import ResetView from '@/components/ResetView.vue'
|
||||||
|
@ -11,11 +11,6 @@
|
|||||||
{{ authStore.isLoading ? 'Authenticating...' : 'Login with Your Device' }}
|
{{ authStore.isLoading ? 'Authenticating...' : 'Login with Your Device' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<p class="toggle-link">
|
|
||||||
<a href="#" @click.prevent="authStore.currentView = 'register'">
|
|
||||||
Don't have an account? Register here
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { formatDate } from '@/utils/helpers'
|
import { formatDate } from '@/utils/helpers'
|
||||||
import { registerCredential } from '@/utils/passkey'
|
import passkey from '@/utils/passkey'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const updateInterval = ref(null)
|
const updateInterval = ref(null)
|
||||||
@ -119,7 +119,7 @@ const addNewCredential = async () => {
|
|||||||
try {
|
try {
|
||||||
authStore.isLoading = true
|
authStore.isLoading = true
|
||||||
authStore.showMessage('Adding new passkey...', 'info')
|
authStore.showMessage('Adding new passkey...', 'info')
|
||||||
const result = await registerCredential()
|
await passkey.register()
|
||||||
await authStore.loadUserInfo()
|
await authStore.loadUserInfo()
|
||||||
authStore.showMessage('New passkey added successfully!', 'success', 3000)
|
authStore.showMessage('New passkey added successfully!', 'success', 3000)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -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>
|
|
@ -3,6 +3,7 @@
|
|||||||
<div class="view active">
|
<div class="view active">
|
||||||
<h1>🔑 Add New Credential</h1>
|
<h1>🔑 Add New Credential</h1>
|
||||||
<h3>👤 {{ authStore.userInfo?.user?.user_name }}</h3>
|
<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>
|
<p>Proceed to complete {{authStore.userInfo?.session_type}}:</p>
|
||||||
<button
|
<button
|
||||||
class="btn-primary"
|
class="btn-primary"
|
||||||
@ -17,7 +18,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { registerCredential } from '@/utils/passkey'
|
import passkey from '@/utils/passkey'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
@ -26,8 +27,7 @@ async function register() {
|
|||||||
authStore.showMessage('Starting registration...', 'info')
|
authStore.showMessage('Starting registration...', 'info')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: For reset sessions, might use registerWithToken() in the future
|
const result = await passkey.register()
|
||||||
const result = await registerCredential()
|
|
||||||
console.log("Result", result)
|
console.log("Result", result)
|
||||||
await authStore.setSessionCookie(result.session_token)
|
await authStore.setSessionCookie(result.session_token)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { registerUser, authenticateUser, registerWithToken } from '@/utils/passkey'
|
import { register, authenticate } from '@/utils/passkey'
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@ -8,7 +8,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
||||||
// UI State
|
// UI State
|
||||||
currentView: 'login', // 'login', 'register', 'profile', 'reset'
|
currentView: 'login', // 'login', 'profile', 'device-link', 'reset'
|
||||||
status: {
|
status: {
|
||||||
message: '',
|
message: '',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -39,23 +39,12 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
async register(user_name) {
|
async register() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
try {
|
try {
|
||||||
const result = await registerUser(user_name)
|
const result = await register()
|
||||||
|
|
||||||
this.userInfo = {
|
|
||||||
user: {
|
|
||||||
user_id: result.user_id,
|
|
||||||
user_name: user_name,
|
|
||||||
},
|
|
||||||
credentials: [],
|
|
||||||
aaguid_info: {},
|
|
||||||
session_type: null,
|
|
||||||
authenticated: false
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.setSessionCookie(result.session_token)
|
await this.setSessionCookie(result.session_token)
|
||||||
|
await this.loadUserInfo()
|
||||||
return result
|
return result
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
@ -64,7 +53,7 @@ export const useAuthStore = defineStore('auth', {
|
|||||||
async authenticate() {
|
async authenticate() {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
try {
|
try {
|
||||||
const result = await authenticateUser()
|
const result = await authenticate()
|
||||||
|
|
||||||
await this.setSessionCookie(result.session_token)
|
await this.setSessionCookie(result.session_token)
|
||||||
await this.loadUserInfo()
|
await this.loadUserInfo()
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
|
import { startRegistration, startAuthentication } from '@simplewebauthn/browser'
|
||||||
import aWebSocket from '@/utils/awaitable-websocket'
|
import aWebSocket from '@/utils/awaitable-websocket'
|
||||||
|
|
||||||
export async function register(url, options) {
|
export async function register() {
|
||||||
if (options) url += `?${new URLSearchParams(options).toString()}`
|
const ws = await aWebSocket("/auth/ws/register")
|
||||||
const ws = await aWebSocket(url)
|
|
||||||
try {
|
try {
|
||||||
const optionsJSON = await ws.receive_json()
|
const optionsJSON = await ws.receive_json()
|
||||||
const registrationResponse = await startRegistration({ optionsJSON })
|
const registrationResponse = await startRegistration({ optionsJSON })
|
||||||
ws.send_json(registrationResponse)
|
ws.send_json(registrationResponse)
|
||||||
const result = await ws.receive_json()
|
return await ws.receive_json()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Registration error:', error)
|
console.error('Registration error:', error)
|
||||||
// Replace useless and ugly error message from startRegistration
|
// Replace useless and ugly error message from startRegistration
|
||||||
@ -18,18 +17,7 @@ export async function register(url, options) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerUser(user_name) {
|
export async function authenticate() {
|
||||||
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() {
|
|
||||||
const ws = await aWebSocket('/auth/ws/authenticate')
|
const ws = await aWebSocket('/auth/ws/authenticate')
|
||||||
try {
|
try {
|
||||||
const optionsJSON = await ws.receive_json()
|
const optionsJSON = await ws.receive_json()
|
||||||
@ -44,3 +32,5 @@ export async function authenticateUser() {
|
|||||||
ws.close()
|
ws.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default { authenticate, register }
|
||||||
|
@ -13,7 +13,7 @@ from datetime import datetime
|
|||||||
import uuid7
|
import uuid7
|
||||||
|
|
||||||
from . import authsession, globals
|
from . import authsession, globals
|
||||||
from .db import Org, Permission, User
|
from .db import Org, Permission, Role, User
|
||||||
from .util import passphrase, tokens
|
from .util import passphrase, tokens
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -54,62 +54,42 @@ async def bootstrap_system(
|
|||||||
dict: Contains information about created entities and reset link
|
dict: Contains information about created entities and reset link
|
||||||
"""
|
"""
|
||||||
# Create permission first - will fail if already exists
|
# Create permission first - will fail if already exists
|
||||||
permission = Permission(id="auth/admin", display_name="Admin")
|
perm0 = Permission(id="auth/admin", display_name="Master Admin")
|
||||||
await globals.db.instance.create_permission(permission)
|
await globals.db.instance.create_permission(perm0)
|
||||||
|
|
||||||
# Create organization
|
org = Org(uuid7.create(), org_name or "Organization")
|
||||||
org_uuid = uuid7.create()
|
|
||||||
org = Org(
|
|
||||||
id=str(org_uuid),
|
|
||||||
options={
|
|
||||||
"display_name": org_name or "Organization",
|
|
||||||
"created_at": datetime.now().isoformat(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await globals.db.instance.create_organization(org)
|
await globals.db.instance.create_organization(org)
|
||||||
|
|
||||||
# Create admin user
|
perm1 = Permission(
|
||||||
admin_uuid = uuid7.create()
|
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(
|
user = User(
|
||||||
uuid=admin_uuid,
|
uuid=uuid7.create(),
|
||||||
display_name=user_name or "Admin",
|
display_name=user_name or "Admin",
|
||||||
org_uuid=org_uuid,
|
role_uuid=role.uuid,
|
||||||
role="Admin",
|
|
||||||
created_at=datetime.now(),
|
created_at=datetime.now(),
|
||||||
visits=0,
|
visits=0,
|
||||||
)
|
)
|
||||||
await globals.db.instance.create_user(user)
|
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
|
# Generate reset link and log it
|
||||||
reset_link = await _create_and_log_admin_reset_link(
|
reset_link = await _create_and_log_admin_reset_link(
|
||||||
admin_uuid, "✅ Bootstrap completed!", "admin bootstrap"
|
user.uuid, "✅ Bootstrap completed!", "admin bootstrap"
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {
|
return {
|
||||||
"admin_user": {
|
"user": user,
|
||||||
"uuid": str(user.uuid),
|
"org": org,
|
||||||
"display_name": user.display_name,
|
"role": role,
|
||||||
"role": user.role,
|
"permissions": [perm0, perm1],
|
||||||
},
|
|
||||||
"organization": {
|
|
||||||
"id": org.id,
|
|
||||||
"display_name": org.options.get("display_name"),
|
|
||||||
},
|
|
||||||
"permission": {
|
|
||||||
"id": permission.id,
|
|
||||||
"display_name": permission.display_name,
|
|
||||||
},
|
|
||||||
"reset_link": reset_link,
|
"reset_link": reset_link,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
async def check_admin_credentials() -> bool:
|
async def check_admin_credentials() -> bool:
|
||||||
"""
|
"""
|
||||||
@ -175,14 +155,7 @@ async def bootstrap_if_needed(
|
|||||||
await globals.db.instance.get_permission("auth/admin")
|
await globals.db.instance.get_permission("auth/admin")
|
||||||
# Permission exists, system is already bootstrapped
|
# Permission exists, system is already bootstrapped
|
||||||
# Check if admin needs credentials (only for already-bootstrapped systems)
|
# Check if admin needs credentials (only for already-bootstrapped systems)
|
||||||
admin_needs_reset = await check_admin_credentials()
|
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",
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception:
|
||||||
# Permission doesn't exist, need to bootstrap
|
# Permission doesn't exist, need to bootstrap
|
||||||
|
@ -11,12 +11,24 @@ from datetime import datetime
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Org:
|
||||||
|
uuid: UUID
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Role:
|
||||||
|
uuid: UUID
|
||||||
|
org_uuid: UUID
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class User:
|
class User:
|
||||||
uuid: UUID
|
uuid: UUID
|
||||||
display_name: str
|
display_name: str
|
||||||
org_uuid: UUID
|
role_uuid: UUID
|
||||||
role: str | None = None
|
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
last_seen: datetime | None = None
|
last_seen: datetime | None = None
|
||||||
visits: int = 0
|
visits: int = 0
|
||||||
@ -35,26 +47,8 @@ class Credential:
|
|||||||
last_verified: datetime | None = None
|
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
|
@dataclass
|
||||||
class Session:
|
class Session:
|
||||||
"""Session data structure."""
|
|
||||||
|
|
||||||
key: bytes
|
key: bytes
|
||||||
user_uuid: UUID
|
user_uuid: UUID
|
||||||
expires: datetime
|
expires: datetime
|
||||||
@ -63,13 +57,17 @@ class Session:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SessionContext:
|
class Permission:
|
||||||
"""Complete session context with user, organization, role, and permissions."""
|
id: str # String primary key (max 128 chars)
|
||||||
|
display_name: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SessionContext:
|
||||||
session: Session
|
session: Session
|
||||||
user: User
|
user: User
|
||||||
organization: Org
|
org: Org
|
||||||
role: str | None = None
|
role: Role
|
||||||
permissions: list[Permission] | None = None
|
permissions: list[Permission] | None = None
|
||||||
|
|
||||||
|
|
||||||
@ -96,6 +94,11 @@ class DatabaseInterface(ABC):
|
|||||||
async def create_user(self, user: User) -> None:
|
async def create_user(self, user: User) -> None:
|
||||||
"""Create a new user."""
|
"""Create a new user."""
|
||||||
|
|
||||||
|
# Role operations
|
||||||
|
@abstractmethod
|
||||||
|
async def create_role(self, role: Role) -> None:
|
||||||
|
"""Create new role."""
|
||||||
|
|
||||||
# Credential operations
|
# Credential operations
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def create_credential(self, credential: Credential) -> None:
|
async def create_credential(self, credential: Credential) -> None:
|
||||||
@ -149,19 +152,19 @@ class DatabaseInterface(ABC):
|
|||||||
|
|
||||||
# Organization operations
|
# Organization operations
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def create_organization(self, organization: Org) -> None:
|
async def create_organization(self, org: Org) -> None:
|
||||||
"""Create a new organization."""
|
"""Add a new organization."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_organization(self, org_id: str) -> Org:
|
async def get_organization(self, org_id: str) -> Org:
|
||||||
"""Get organization by ID."""
|
"""Get organization by ID."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def update_organization(self, organization: Org) -> None:
|
async def update_organization(self, org: Org) -> None:
|
||||||
"""Update organization options."""
|
"""Update organization options."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def delete_organization(self, org_id: str) -> None:
|
async def delete_organization(self, org_uuid: UUID) -> None:
|
||||||
"""Delete organization by ID."""
|
"""Delete organization by ID."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
@ -29,6 +29,7 @@ from . import (
|
|||||||
DatabaseInterface,
|
DatabaseInterface,
|
||||||
Org,
|
Org,
|
||||||
Permission,
|
Permission,
|
||||||
|
Role,
|
||||||
Session,
|
Session,
|
||||||
SessionContext,
|
SessionContext,
|
||||||
User,
|
User,
|
||||||
@ -42,41 +43,47 @@ async def init(*args, **kwargs):
|
|||||||
await db.instance.init_db()
|
await db.instance.init_db()
|
||||||
|
|
||||||
|
|
||||||
# SQLAlchemy Models
|
|
||||||
class Base(DeclarativeBase):
|
class Base(DeclarativeBase):
|
||||||
pass
|
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):
|
class OrgModel(Base):
|
||||||
__tablename__ = "orgs"
|
__tablename__ = "orgs"
|
||||||
|
|
||||||
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
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):
|
class UserModel(Base):
|
||||||
@ -84,17 +91,38 @@ class UserModel(Base):
|
|||||||
|
|
||||||
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
||||||
display_name: Mapped[str] = mapped_column(String, nullable=False)
|
display_name: Mapped[str] = mapped_column(String, nullable=False)
|
||||||
org_uuid: Mapped[bytes] = mapped_column(
|
role_uuid: Mapped[bytes] = mapped_column(
|
||||||
LargeBinary(16), ForeignKey("orgs.uuid", ondelete="CASCADE"), nullable=False
|
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)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now)
|
||||||
last_seen: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
last_seen: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||||
visits: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
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):
|
class CredentialModel(Base):
|
||||||
__tablename__ = "credentials"
|
__tablename__ = "credentials"
|
||||||
|
|
||||||
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
uuid: Mapped[bytes] = mapped_column(LargeBinary(16), primary_key=True)
|
||||||
credential_id: Mapped[bytes] = mapped_column(
|
credential_id: Mapped[bytes] = mapped_column(
|
||||||
LargeBinary(64), unique=True, index=True
|
LargeBinary(64), unique=True, index=True
|
||||||
@ -121,7 +149,73 @@ class SessionModel(Base):
|
|||||||
LargeBinary(16), ForeignKey("credentials.uuid", ondelete="CASCADE")
|
LargeBinary(16), ForeignKey("credentials.uuid", ondelete="CASCADE")
|
||||||
)
|
)
|
||||||
expires: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
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):
|
class DB(DatabaseInterface):
|
||||||
@ -155,29 +249,16 @@ class DB(DatabaseInterface):
|
|||||||
user_model = result.scalar_one_or_none()
|
user_model = result.scalar_one_or_none()
|
||||||
|
|
||||||
if user_model:
|
if user_model:
|
||||||
return User(
|
return user_model.as_dataclass()
|
||||||
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,
|
|
||||||
)
|
|
||||||
raise ValueError("User not found")
|
raise ValueError("User not found")
|
||||||
|
|
||||||
async def create_user(self, user: User) -> None:
|
async def create_user(self, user: User) -> None:
|
||||||
async with self.session() as session:
|
async with self.session() as session:
|
||||||
user_model = UserModel(
|
session.add(UserModel.from_dataclass(user))
|
||||||
uuid=user.uuid.bytes,
|
|
||||||
display_name=user.display_name,
|
async def create_role(self, role: Role) -> None:
|
||||||
org_uuid=user.org_uuid.bytes,
|
async with self.session() as session:
|
||||||
role=user.role,
|
session.add(RoleModel.from_dataclass(role))
|
||||||
created_at=user.created_at or datetime.now(),
|
|
||||||
last_seen=user.last_seen,
|
|
||||||
visits=user.visits,
|
|
||||||
)
|
|
||||||
session.add(user_model)
|
|
||||||
|
|
||||||
async def create_credential(self, credential: Credential) -> None:
|
async def create_credential(self, credential: Credential) -> None:
|
||||||
async with self.session() as session:
|
async with self.session() as session:
|
||||||
@ -265,19 +346,8 @@ class DB(DatabaseInterface):
|
|||||||
self, user: User, credential: Credential
|
self, user: User, credential: Credential
|
||||||
) -> None:
|
) -> None:
|
||||||
async with self.session() as session:
|
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
|
# Create user
|
||||||
user_model = UserModel(
|
user_model = UserModel.from_dataclass(user)
|
||||||
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(user_model)
|
||||||
|
|
||||||
# Create credential
|
# Create credential
|
||||||
@ -352,13 +422,11 @@ class DB(DatabaseInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Organization operations
|
# Organization operations
|
||||||
async def create_organization(self, organization: Org) -> None:
|
async def create_organization(self, org: Org) -> None:
|
||||||
async with self.session() as session:
|
async with self.session() as session:
|
||||||
# Convert string ID to UUID bytes for storage
|
|
||||||
org_uuid = UUID(organization.id)
|
|
||||||
org_model = OrgModel(
|
org_model = OrgModel(
|
||||||
uuid=org_uuid.bytes,
|
uuid=org.uuid.bytes,
|
||||||
options=organization.options,
|
display_name=org.display_name,
|
||||||
)
|
)
|
||||||
session.add(org_model)
|
session.add(org_model)
|
||||||
|
|
||||||
@ -370,34 +438,28 @@ class DB(DatabaseInterface):
|
|||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
org_model = result.scalar_one_or_none()
|
org_model = result.scalar_one_or_none()
|
||||||
|
|
||||||
if org_model:
|
if not 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")
|
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:
|
async with self.session() as session:
|
||||||
# Convert string ID to UUID bytes for lookup
|
|
||||||
org_uuid = UUID(organization.id)
|
|
||||||
stmt = (
|
stmt = (
|
||||||
update(OrgModel)
|
update(OrgModel)
|
||||||
.where(OrgModel.uuid == org_uuid.bytes)
|
.where(OrgModel.uuid == org.uuid.bytes)
|
||||||
.values(options=organization.options)
|
.values(display_name=org.display_name)
|
||||||
)
|
)
|
||||||
await session.execute(stmt)
|
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:
|
async with self.session() as session:
|
||||||
# Convert string ID to UUID bytes for lookup
|
# Convert string ID to UUID bytes for lookup
|
||||||
org_uuid = UUID(org_id)
|
|
||||||
stmt = delete(OrgModel).where(OrgModel.uuid == org_uuid.bytes)
|
stmt = delete(OrgModel).where(OrgModel.uuid == org_uuid.bytes)
|
||||||
await session.execute(stmt)
|
await session.execute(stmt)
|
||||||
|
|
||||||
async def add_user_to_organization(
|
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:
|
) -> None:
|
||||||
async with self.session() as session:
|
async with self.session() as session:
|
||||||
# Get user and organization models
|
# Get user and organization models
|
||||||
@ -406,7 +468,6 @@ class DB(DatabaseInterface):
|
|||||||
user_model = user_result.scalar_one_or_none()
|
user_model = user_result.scalar_one_or_none()
|
||||||
|
|
||||||
# Convert string ID to UUID bytes for lookup
|
# Convert string ID to UUID bytes for lookup
|
||||||
org_uuid = UUID(org_id)
|
|
||||||
org_stmt = select(OrgModel).where(OrgModel.uuid == org_uuid.bytes)
|
org_stmt = select(OrgModel).where(OrgModel.uuid == org_uuid.bytes)
|
||||||
org_result = await session.execute(org_stmt)
|
org_result = await session.execute(org_stmt)
|
||||||
org_model = org_result.scalar_one_or_none()
|
org_model = org_result.scalar_one_or_none()
|
||||||
@ -449,7 +510,7 @@ class DB(DatabaseInterface):
|
|||||||
if result.rowcount == 0:
|
if result.rowcount == 0:
|
||||||
raise ValueError("User not found")
|
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:
|
async with self.session() as session:
|
||||||
stmt = select(UserModel).where(UserModel.uuid == user_uuid.bytes)
|
stmt = select(UserModel).where(UserModel.uuid == user_uuid.bytes)
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
@ -459,37 +520,19 @@ class DB(DatabaseInterface):
|
|||||||
raise ValueError("User not found")
|
raise ValueError("User not found")
|
||||||
|
|
||||||
# Fetch the organization details
|
# 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_result = await session.execute(org_stmt)
|
||||||
org_model = org_result.scalar_one()
|
org_model = org_result.scalar_one()
|
||||||
|
|
||||||
# Convert UUID bytes back to string for the interface
|
# Convert UUID bytes back to string for the interface
|
||||||
org = Org(id=str(UUID(bytes=org_model.uuid)), options=org_model.options)
|
return org_model.as_dataclass()
|
||||||
return (org, user_model.role or "")
|
|
||||||
|
|
||||||
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:
|
async with self.session() as session:
|
||||||
# Convert string ID to UUID bytes for lookup
|
stmt = select(UserModel).where(UserModel.role_uuid == role.uuid.bytes)
|
||||||
org_uuid = UUID(org_id)
|
|
||||||
stmt = select(UserModel).where(UserModel.org_uuid == org_uuid.bytes)
|
|
||||||
result = await session.execute(stmt)
|
result = await session.execute(stmt)
|
||||||
user_models = result.scalars().all()
|
user_models = result.scalars().all()
|
||||||
|
return [u.as_dataclass() for u in user_models]
|
||||||
# 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
|
|
||||||
|
|
||||||
async def get_user_role_in_organization(
|
async def get_user_role_in_organization(
|
||||||
self, user_uuid: UUID, org_id: str
|
self, user_uuid: UUID, org_id: str
|
||||||
@ -638,8 +681,7 @@ class DB(DatabaseInterface):
|
|||||||
org_model = org_result.scalar_one()
|
org_model = org_result.scalar_one()
|
||||||
|
|
||||||
# Convert UUID bytes back to string for the interface
|
# 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.as_dataclass())
|
||||||
organizations.append(org)
|
|
||||||
|
|
||||||
return organizations
|
return organizations
|
||||||
|
|
||||||
@ -695,21 +737,10 @@ class DB(DatabaseInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Create the user object
|
# Create the user object
|
||||||
user_obj = User(
|
user_obj = user_model.as_dataclass()
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create organization object (always exists now)
|
# Create organization object (always exists now)
|
||||||
organization = Org(
|
organization = Org(UUID(bytes=org_model.uuid), org_model.display_name)
|
||||||
id=str(UUID(bytes=org_model.uuid)),
|
|
||||||
options=org_model.options,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Collect all unique permissions
|
# Collect all unique permissions
|
||||||
permissions = []
|
permissions = []
|
||||||
@ -728,7 +759,7 @@ class DB(DatabaseInterface):
|
|||||||
return SessionContext(
|
return SessionContext(
|
||||||
session=session_obj,
|
session=session_obj,
|
||||||
user=user_obj,
|
user=user_obj,
|
||||||
organization=organization,
|
org=organization,
|
||||||
role=user_model.role,
|
role=user_model.role,
|
||||||
permissions=permissions if permissions else None,
|
permissions=permissions if permissions else None,
|
||||||
)
|
)
|
||||||
|
@ -1,17 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import uuid7
|
from fastapi import Cookie, FastAPI, WebSocket, WebSocketDisconnect
|
||||||
from fastapi import Cookie, FastAPI, Query, WebSocket, WebSocketDisconnect
|
|
||||||
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
|
from webauthn.helpers.exceptions import InvalidAuthenticationResponse
|
||||||
|
|
||||||
from ..authsession import EXPIRES, create_session, get_reset, get_session
|
from ..authsession import create_session, get_reset, get_session
|
||||||
from ..db import User
|
|
||||||
from ..globals import db, passkey
|
from ..globals import db, passkey
|
||||||
from ..util import passphrase
|
from ..util import passphrase
|
||||||
from ..util.tokens import create_token, session_key
|
|
||||||
from .session import infodict
|
from .session import infodict
|
||||||
|
|
||||||
|
|
||||||
@ -58,40 +54,6 @@ async def register_chat(
|
|||||||
|
|
||||||
@app.websocket("/register")
|
@app.websocket("/register")
|
||||||
@websocket_error_handler
|
@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)):
|
async def websocket_register_add(ws: WebSocket, auth=Cookie(None)):
|
||||||
"""Register a new credential for an existing user."""
|
"""Register a new credential for an existing user."""
|
||||||
origin = ws.headers["origin"]
|
origin = ws.headers["origin"]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user