Modified id usage to uuids, reset migrations, removed token from invites

This commit is contained in:
Viswamedha Nalabotu 2026-02-27 12:53:19 +00:00
parent 529ab95a91
commit c362c79912
18 changed files with 78 additions and 154 deletions

View file

@ -37,11 +37,11 @@ class OrganizationAdmin(ModelAdmin):
@admin.register(Invite)
class InviteAdmin(ModelAdmin):
list_display = ('token', 'organization', 'created_by', 'is_active', 'uses', 'max_uses', 'expires_at')
search_fields = ('token', 'organization__name', 'created_by__email_address')
list_display = ('uuid', 'organization', 'created_by', 'is_active', 'uses', 'max_uses', 'expires_at')
search_fields = ('uuid', 'organization__name', 'created_by__email_address')
list_filter = ('is_active', 'expires_at')
raw_id_fields = ('organization', 'created_by')
readonly_fields = ('token', 'created_at')
readonly_fields = ('uuid', 'created_at')
@admin.register(Role)
class RoleAdmin(ModelAdmin):

View file

@ -1,7 +1,8 @@
import django.db.models.deletion
import uuid
import uuid
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
@ -61,17 +62,16 @@ class Migration(migrations.Migration):
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='Token')),
('expires_at', models.DateTimeField(verbose_name='Expires At')),
('uses', models.IntegerField(default=0, verbose_name='Uses')),
('max_uses', models.IntegerField(default=1, verbose_name='Max Uses')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='created_invites', to=settings.AUTH_USER_MODEL)),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invite_tokens', to='accounts.organization')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='accounts.organization')),
],
options={
'verbose_name': 'Invite Token',
'verbose_name_plural': 'Invite Tokens',
'verbose_name': 'Invite',
'verbose_name_plural': 'Invites',
},
),
migrations.CreateModel(

View file

@ -1,23 +0,0 @@
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='invite',
options={'verbose_name': 'Invite', 'verbose_name_plural': 'Invites'},
),
migrations.AlterField(
model_name='invite',
name='organization',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='accounts.organization'),
),
]

View file

@ -64,8 +64,7 @@ class Organization(IdentifierMixin, TimeStampMixin, Model):
return self.name
class Invite(IdentifierMixin, TimeStampMixin, Model):
token = UUIDField(verbose_name = _("Token"), default = uuid4, unique = True, editable = False)
organization = ForeignKey(Organization, on_delete = CASCADE, related_name = "invites")
created_by = ForeignKey(User, on_delete = CASCADE, related_name = "created_invites")
expires_at = DateTimeField(verbose_name=_("Expires At"))

View file

@ -32,14 +32,14 @@ class InviteSerializer(ModelSerializer):
class Meta:
model = Invite
fields = ['id', 'token', 'organization', 'created_by', 'expires_at', 'uses', 'max_uses', 'is_active', 'created_at', 'updated_at', 'invite_url', 'is_valid']
read_only_fields = ['id', 'token', 'organization', 'created_by', 'created_at', 'updated_at']
fields = ['id', 'uuid', 'organization', 'created_by', 'expires_at', 'uses', 'max_uses', 'is_active', 'created_at', 'updated_at', 'invite_url', 'is_valid']
read_only_fields = ['id', 'uuid', 'organization', 'created_by', 'created_at', 'updated_at']
def get_invite_url(self, obj: Invite) -> str:
request = self.context.get('request')
if request:
return request.build_absolute_uri(f'/invite/{obj.token}')
return f'/invite/{obj.token}'
return request.build_absolute_uri(f'/invite/{obj.uuid}')
return f'/invite/{obj.uuid}'
def get_is_valid(self, obj: Invite) -> bool:
return obj.is_valid()

View file

@ -140,30 +140,30 @@ class OrganizationViewSet(ModelViewSet):
)
return Response(InviteSerializer(invitation, context={'request': request}).data)
@action(detail=True, methods=['delete'], url_path=r'revoke-invite/(?P<token>[0-9a-f-]{36})')
def revoke_invite(self, request, uuid=None, token=None):
@action(detail=True, methods=['delete'], url_path=r'revoke-invite/(?P<invite_uuid>[0-9a-f-]{36})')
def revoke_invite(self, request, uuid=None, invite_uuid=None):
organization = self.get_object()
if not request.user.is_manager:
return Response({'error': 'Only managers can revoke invites'}, status=HTTP_403_FORBIDDEN)
invite = organization.invites.filter(token=token).first()
invite = organization.invites.filter(uuid=invite_uuid).first()
if not invite:
return Response({'error': 'Invalid invitation token or not found in this organization'}, status=HTTP_404_NOT_FOUND)
return Response({'error': 'Invalid invitation uuid or not found in this organization'}, status=HTTP_404_NOT_FOUND)
invite.is_active = False
invite.save()
return Response({'message': 'Invitation successfully revoked'}, status=HTTP_200_OK)
@action(detail=False, methods=['post'], url_path='join/(?P<token>[0-9a-f-]{36})')
def join(self, request, token=None):
@action(detail=False, methods=['post'], url_path='join/(?P<invite_uuid>[0-9a-f-]{36})')
def join(self, request, invite_uuid=None):
try:
invitation = Invite.objects.get(token=token)
invitation = Invite.objects.get(uuid=invite_uuid)
except Invite.DoesNotExist:
return Response({'error': 'Not Found'}, status=HTTP_404_NOT_FOUND)
if not invitation.is_valid():
return Response({'error': 'Invalid or expired token'}, status=HTTP_400_BAD_REQUEST)
return Response({'error': 'Invalid or expired invitation'}, status=HTTP_400_BAD_REQUEST)
organization = invitation.organization
if organization.members.filter(id=request.user.id).exists():
if organization.members.filter(uuid=request.user.uuid).exists():
return Response({'error': 'Already a member'}, status=HTTP_403_FORBIDDEN)
organization.members.add(request.user)
@ -184,7 +184,7 @@ class OrganizationViewSet(ModelViewSet):
if organization.owner == request.user:
return Response({'error': 'Owner cannot leave'}, status=HTTP_403_FORBIDDEN)
if not organization.members.filter(id=request.user.id).exists():
if not organization.members.filter(uuid=request.user.uuid).exists():
return Response({'error': 'Not a member'}, status=HTTP_400_BAD_REQUEST)
organization.members.remove(request.user)
@ -196,16 +196,16 @@ class OrganizationViewSet(ModelViewSet):
serializer = UserSerializer(organization.members.all(), many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'], url_path=r'member/(?P<user_id>\d+)/remove')
def remove_member(self, request, uuid=None, user_id=None):
@action(detail=True, methods=['post'], url_path=r'member/(?P<user_uuid>[0-9a-f-]{36})/remove')
def remove_member(self, request, uuid=None, user_uuid=None):
if not request.user.is_manager:
return Response({'error': 'Forbidden'}, status=HTTP_403_FORBIDDEN)
organization = self.get_object()
if str(organization.owner.id) == str(user_id):
if str(organization.owner.uuid) == str(user_uuid):
return Response({'error': 'Cannot remove owner'}, status=HTTP_403_FORBIDDEN)
user_to_remove = organization.members.filter(id=user_id).first()
user_to_remove = organization.members.filter(uuid=user_uuid).first()
if not user_to_remove:
return Response({'error': 'Not found'}, status=HTTP_404_NOT_FOUND)

View file

@ -1,9 +1,9 @@
import django.db.models.deletion
import pgvector.django
import uuid
from django.conf import settings
from django.db import migrations, models
from pgvector.django import VectorExtension
import django.db.models.deletion
import pgvector.django.vector
class Migration(migrations.Migration):
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
]
operations = [
VectorExtension(),
pgvector.django.VectorExtension(),
migrations.CreateModel(
name='TrainingFile',
fields=[
@ -48,7 +48,7 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('content', models.TextField()),
('content_hash', models.CharField(db_index=True, max_length=64)),
('embedding', pgvector.django.VectorField(blank=True, dimensions=1536, null=True)),
('embedding', pgvector.django.vector.VectorField(blank=True, dimensions=1536, null=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('chunk_index', models.IntegerField(default=0)),
('is_active', models.BooleanField(default=True)),

View file

@ -22,9 +22,9 @@ class Migration(migrations.Migration):
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('name', models.CharField(max_length=255, verbose_name='Agent Name')),
('agent_type', models.CharField(choices=[('curriculum', 'Curriculum Agent (CA)'), ('knowledge', 'Knowledge Agent (KA)'), ('assessment', 'Assessment Agent (AA)'), ('monitor', 'Progress Monitor Agent (PMA)')], max_length=40, verbose_name='Agent Type')),
('llm_config', models.JSONField(default=dict, verbose_name='LLM Configuration')),
('system_prompt', models.TextField(verbose_name='System Prompt')),
('tool_permissions', models.JSONField(default=list, verbose_name='Tool Permissions')),
('llm_config', models.JSONField(blank=True, default=dict, null=True, verbose_name='LLM Configuration')),
('system_prompt', models.TextField(blank=True, default='', verbose_name='System Prompt')),
('tool_permissions', models.JSONField(blank=True, default=list, null=True, verbose_name='Tool Permissions')),
('organization', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='agent_configs', to='accounts.organization', verbose_name='Organization')),
],
options={
@ -40,6 +40,7 @@ class Migration(migrations.Migration):
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('title', models.CharField(max_length=255, verbose_name='Flow Title')),
('structure', models.JSONField(blank=True, default=list, verbose_name='Flow Structure')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='flows', to='accounts.role', verbose_name='Role')),
],

View file

@ -1,28 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('onboarding', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='agentconfig',
name='llm_config',
field=models.JSONField(blank=True, default=dict, null=True, verbose_name='LLM Configuration'),
),
migrations.AlterField(
model_name='agentconfig',
name='system_prompt',
field=models.TextField(blank=True, default='', verbose_name='System Prompt'),
),
migrations.AlterField(
model_name='agentconfig',
name='tool_permissions',
field=models.JSONField(blank=True, default=list, null=True, verbose_name='Tool Permissions'),
),
]

View file

@ -1,18 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('onboarding', '0002_alter_agentconfig_llm_config_and_more'),
]
operations = [
migrations.AddField(
model_name='onboardingflow',
name='structure',
field=models.JSONField(blank=True, default=list, verbose_name='Flow Structure'),
),
]

View file

@ -94,8 +94,8 @@ export const API = {
byId: (uuid: string) => `organization/${uuid}/`,
members: {
list: (uuid: string) => `organization/${uuid}/members/`,
remove: (uuid: string, userId: number) =>
`organization/${uuid}/member/${userId}/remove/`,
remove: (uuid: string, userUuid: string) =>
`organization/${uuid}/member/${userUuid}/remove/`,
},
invites: {
list: (uuid: string) => `organization/${uuid}/invite/`,
@ -103,7 +103,7 @@ export const API = {
`organization/${uuid}/create-invite/?max_uses=${maxUses}`,
revoke: (uuid: string, inviteUuid: string) =>
`organization/${uuid}/revoke-invite/${inviteUuid}/`,
join: (token: string) => `organization/join/${token}/`,
join: (inviteUuid: string) => `organization/join/${inviteUuid}/`,
},
leave: (uuid: string) => `organization/${uuid}/leave/`,
roles: {

View file

@ -39,19 +39,19 @@ const router = createRouter({
meta: { requiresAuth: true },
},
{
path: '/organization/:id',
path: '/organization/:organizationUuid',
name: 'organization-detail',
component: () => import('../views/OrganizationView.vue'),
meta: { requiresAuth: true },
},
{
path: '/organization/:id/manage',
path: '/organization/:organizationUuid/manage',
name: 'organization-manage',
component: () => import('../views/OrganizationManage.vue'),
meta: { requiresAuth: true, requiresManager: true },
},
{
path: '/invite/:token',
path: '/invite/:inviteUuid',
name: 'invite-accept',
component: () => import('../views/InviteAccept.vue'),
},
@ -62,7 +62,7 @@ const router = createRouter({
meta: { requiresAuth: true, requiresManager: true },
},
{
path: '/agents/:id',
path: '/agents/:agentUuid',
name: 'agent-detail',
component: () => import('../views/AgentDetailView.vue'),
meta: { requiresAuth: true, requiresManager: true },

View file

@ -1,7 +1,6 @@
import { User } from './user'
export interface Organization {
id: number
uuid: string
name: string
description: string
@ -13,7 +12,6 @@ export interface Organization {
}
export interface Role {
id: number
uuid: string
name: string
description?: string
@ -24,8 +22,7 @@ export interface Role {
}
export interface InviteToken {
id: number
token: string
uuid: string
invite_url: string
created_by: User
organization: Organization
@ -36,7 +33,6 @@ export interface InviteToken {
uses?: number
}
export interface TrainingFile {
id: number
uuid: string
role: Role
uploaded_by: User

View file

@ -1,5 +1,4 @@
export interface User {
id: number
uuid: string
email_address: string
first_name: string

View file

@ -7,7 +7,7 @@ import { apiClient, isAxiosError, API } from '../router/api'
const route = useRoute()
const router = useRouter()
const token = route.params.token as string
const inviteUuid = route.params.inviteUuid as string
const loading = ref(false)
const accepting = ref(false)
const accepted = ref(false)
@ -18,7 +18,7 @@ const acceptInvite = async () => {
error.value = null
try {
const response = await apiClient.post<{ message: string; success: boolean; uuid: string }>(
API.organization.invites.join(token),
API.organization.invites.join(inviteUuid),
)
message.success(response.data?.message || 'Successfully joined organization')
accepted.value = true

View file

@ -26,7 +26,7 @@ const route = useRoute()
const router = useRouter()
const auth = useUserStore()
const orgId = route.params.id as string
const organizationUuid = route.params.organizationUuid as string
const organization = ref<Organization | null>(null)
const members = ref<User[]>([])
const invites = ref<InviteToken[]>([])
@ -48,7 +48,7 @@ const newDescription = ref('')
const fetchOrganization = async () => {
loading.value = true
try {
const response = await apiClient.get<Organization>(API.organization.byId(orgId))
const response = await apiClient.get<Organization>(API.organization.byId(organizationUuid))
organization.value = response.data
newDescription.value = response.data.description
} catch (error) {
@ -61,7 +61,7 @@ const fetchOrganization = async () => {
const fetchMembers = async () => {
try {
const response = await apiClient.get<User[]>(API.organization.members.list(orgId))
const response = await apiClient.get<User[]>(API.organization.members.list(organizationUuid))
members.value = response.data
} catch (error) {
console.error('Failed to fetch members:', error)
@ -70,7 +70,7 @@ const fetchMembers = async () => {
const fetchInvites = async () => {
try {
const response = await apiClient.get<InviteToken[]>(API.organization.invites.list(orgId))
const response = await apiClient.get<InviteToken[]>(API.organization.invites.list(organizationUuid))
invites.value = response.data
} catch (error) {
console.error('Failed to fetch invites:', error)
@ -79,7 +79,7 @@ const fetchInvites = async () => {
const fetchRoles = async () => {
try {
const response = await apiClient.get<Role[]>(API.organization.roles.list(orgId))
const response = await apiClient.get<Role[]>(API.organization.roles.list(organizationUuid))
Roles.value = response.data as unknown as Role[]
} catch (error) {
console.error('Failed to fetch Roles:', error)
@ -109,7 +109,7 @@ const createRole = async () => {
creatingRole.value = true
try {
await apiClient.post(API.organization.roles.list(orgId), { name, description })
await apiClient.post(API.organization.roles.list(organizationUuid), { name, description })
message.success('Role created successfully')
roleModalVisible.value = false
resetRoleForm()
@ -136,7 +136,7 @@ const deleteRole = async (role: Role) => {
onOk: async () => {
deletingRoleUuid.value = role.uuid
try {
await apiClient.delete(API.organization.roles.remove(orgId, role.uuid))
await apiClient.delete(API.organization.roles.remove(organizationUuid, role.uuid))
message.success('Role deleted successfully')
await fetchRoles()
} catch (error) {
@ -156,7 +156,7 @@ const deleteRole = async (role: Role) => {
const createInvite = async () => {
try {
const response = await apiClient.post<InviteToken>(
API.organization.invites.create(orgId, newInviteMaxUses.value),
API.organization.invites.create(organizationUuid, newInviteMaxUses.value),
)
newInviteUrl.value = response.data.invite_url
inviteModalVisible.value = true
@ -177,9 +177,9 @@ const copyUrl = (url: string) => {
message.success('Copied to clipboard')
}
const revokeInvite = async (token: string) => {
const revokeInvite = async (inviteUuid: string) => {
try {
await apiClient.delete(API.organization.invites.revoke(orgId, token))
await apiClient.delete(API.organization.invites.revoke(organizationUuid, inviteUuid))
message.success('Invite revoked')
fetchInvites()
} catch (error) {
@ -188,9 +188,9 @@ const revokeInvite = async (token: string) => {
}
}
const removeMember = async (userId: number) => {
const removeMember = async (userUuid: string) => {
try {
await apiClient.post(API.organization.members.remove(orgId, userId))
await apiClient.post(API.organization.members.remove(organizationUuid, userUuid))
message.success('Member removed')
fetchMembers()
} catch (error) {
@ -203,7 +203,7 @@ const removeMember = async (userId: number) => {
const saveDescription = async () => {
try {
await apiClient.patch(API.organization.byId(orgId), {
await apiClient.patch(API.organization.byId(organizationUuid), {
description: newDescription.value,
})
message.success('Description updated')
@ -221,14 +221,14 @@ onMounted(async () => {
await fetchInvites()
await fetchRoles()
const currentUserId = auth.user?.id
const isOwner = organization.value?.owner?.id === currentUserId
const myMembership = members.value.find((m) => m.id === currentUserId)
const currentUserUuid = auth.user?.uuid
const isOwner = organization.value?.owner?.uuid === currentUserUuid
const myMembership = members.value.find((member) => member.uuid === currentUserUuid)
const isEmployer = myMembership?.is_manager
if (!isOwner && !isEmployer) {
message.error('You do not have permission to manage this organization')
router.replace(`/organization/${orgId}`)
router.replace(`/organization/${organizationUuid}`)
}
})
</script>
@ -239,7 +239,7 @@ onMounted(async () => {
<Card v-if="organization" class="panel" :bordered="false">
<div class="header">
<Typography.Title :level="2">Manage {{ organization.name }}</Typography.Title>
<Button type="default" @click="router.push(`/organization/${orgId}`)">
<Button type="default" @click="router.push(`/organization/${organizationUuid}`)">
Back to Organization
</Button>
</div>
@ -289,10 +289,10 @@ onMounted(async () => {
/>
<Space>
<Button
v-if="item.id !== organization.owner.id"
v-if="item.uuid !== organization.owner.uuid"
danger
size="small"
@click="removeMember(item.id)"
@click="removeMember(item.uuid)"
>
Remove
</Button>
@ -343,7 +343,7 @@ onMounted(async () => {
<Button
danger
size="small"
@click="revokeInvite(item.token)"
@click="revokeInvite(item.uuid)"
>
Revoke
</Button>

View file

@ -23,11 +23,11 @@ import type { Role, Organization, TrainingFile } from '../types/organization'
const router = useRouter()
const route = useRoute()
const orgId = route.params.id as string
const organizationUuid = route.params.organizationUuid as string
const organization = ref<Organization | null>(null)
const roles = ref<Role[]>([])
const members = ref<Array<{ user: { id: number }; role: string }>>([])
const members = ref<Array<{ uuid: string; is_manager?: boolean }>>([])
const trainingFiles = ref<TrainingFile[]>([])
const loading = ref(false)
const uploading = ref(false)
@ -38,14 +38,14 @@ const isManager = computed(() => {
if (!auth.user || !organization.value) return false
if ((organization.value as Organization & { is_manager?: boolean }).is_manager === true)
return true
if (organization.value.owner?.id === auth.user.id) return true
return members.value.some((m) => m.user?.id === auth.user?.id && m.role === 'employer')
if (organization.value.owner?.uuid === auth.user.uuid) return true
return members.value.some((member) => member.uuid === auth.user?.uuid && member.is_manager)
})
const fetchOrganization = async () => {
loading.value = true
try {
const response = await apiClient.get<Organization>(API.organization.byId(orgId))
const response = await apiClient.get<Organization>(API.organization.byId(organizationUuid))
organization.value = response.data
} catch (error) {
console.error('Failed to fetch organization:', error)
@ -86,7 +86,7 @@ const isRoleJoined = (roleUuid: string | undefined) => {
const fetchMembers = async () => {
if (!organization.value?.uuid) return
try {
const response = await apiClient.get<Array<{ user: { id: number }; role: string }>>(
const response = await apiClient.get<Array<{ uuid: string; is_manager?: boolean }>>(
API.organization.members.list(organization.value.uuid),
)
members.value = response.data
@ -100,7 +100,7 @@ const selectRole = async (roleUuid: string) => {
message.error('Organization not loaded')
return
}
if (!auth.user?.id) {
if (!auth.user?.uuid) {
try {
await auth.fetchSession(true)
} catch {
@ -315,7 +315,7 @@ const trainingFileColumns = [
title: 'Action',
key: 'action',
customRender: ({ record }: { record: TrainingFile }) => {
if (isManager.value || auth.user?.id === record.uploaded_by?.id) {
if (isManager.value || auth.user?.uuid === record.uploaded_by?.uuid) {
return h(
Button,
{

View file

@ -25,8 +25,7 @@ const fetchOrganizations = async () => {
if (organizations.value.length === 1 && !auth.isGeneralManager) {
const onlyOrg = organizations.value[0]
const id = onlyOrg.uuid || String(onlyOrg.id)
await router.replace(`/organization/${id}`)
await router.replace(`/organization/${onlyOrg.uuid}`)
}
} catch (err: unknown) {
console.error('Failed to fetch organizations:', err)
@ -52,8 +51,7 @@ onMounted(() => {
})
const openOrg = (org: Organization) => {
const id = org.uuid || String(org.id)
router.push(`/organization/${id}`)
router.push(`/organization/${org.uuid}`)
}
const resetCreateOrganizationForm = () => {