Coverage for core/init_db.py: 100.00%
124 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-03 15:30 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-03 15:30 +0000
1import logging
3from sqlalchemy import delete, select, text
5from core.config import settings
6from core.database import AsyncSessionLocal
7from core.permissions import get_attributes
8from core.security import hash_password
9from models.role_attributes import RoleAttributes
10from models.role_mapper import RoleMapper
11from models.roles import Roles
12from models.users import Users
14logger = logging.getLogger("init_db")
17async def is_already_initialized() -> bool:
18 """Return True if super admin role and user already exist (seed done)."""
19 async with AsyncSessionLocal() as db:
20 # Super admin role must exist
21 super_role_result = await db.execute(
22 select(Roles).where(Roles.name == settings.DEFAULT_SUPER_ADMIN_ROLE)
23 )
24 super_role = super_role_result.scalar_one_or_none()
25 if not super_role:
26 return False
28 # At least one attribute must exist
29 attrs_result = await db.execute(select(RoleAttributes))
30 if not attrs_result.scalars().first():
31 return False
33 # If any user already mapped to super admin role, consider seeded
34 super_users_result = await db.execute(
35 select(Users)
36 .join(RoleMapper, Users.id == RoleMapper.user_id)
37 .where(RoleMapper.role_id == super_role.id)
38 )
39 if super_users_result.scalars().first():
40 return True
42 return False
45async def init_database():
46 """Initialize database with default data: role attributes, roles and admin account"""
47 lock_name = "init_db_lock"
48 lock_timeout_sec = 15
50 # Use DB advisory lock to avoid multiple workers seeding simultaneously
51 async with AsyncSessionLocal() as lock_session:
52 lock_result = await lock_session.execute(
53 text("SELECT GET_LOCK(:name, :timeout)"),
54 {"name": lock_name, "timeout": lock_timeout_sec},
55 )
56 lock_acquired = lock_result.scalar()
58 if lock_acquired != 1:
59 logger.info("Skipping database initialization: another worker is seeding.")
60 return
62 try:
63 # Double-check after getting the lock, if already initialized, skip
64 if await is_already_initialized():
65 logger.info("Database initialization already completed, skipping.")
66 return
68 logger.info("Starting database initialization...")
70 await create_role_attributes()
71 await create_default_roles()
72 await create_default_admin()
74 logger.info("Database initialization completed")
76 except Exception as e:
77 logger.error(f"Database initialization failed: {str(e)}")
78 raise
79 finally:
80 # Always release the advisory lock
81 await lock_session.execute(text("SELECT RELEASE_LOCK(:name)"), {"name": lock_name})
82 await lock_session.commit()
85async def create_role_attributes():
86 """Create role attributes"""
87 async with AsyncSessionLocal() as db:
88 try:
89 attributes = get_attributes()
90 created_count = 0
91 updated_count = 0
93 for attr_config in attributes:
94 existing_attr = await db.execute(
95 select(RoleAttributes).where(RoleAttributes.name == attr_config["name"])
96 )
97 existing = existing_attr.scalar_one_or_none()
98 if existing:
99 if (
100 getattr(existing, "group", None) is None
101 and attr_config.get("group") is not None
102 ):
103 existing.group = attr_config.get("group")
104 updated_count += 1
105 if (
106 getattr(existing, "category", None) is None
107 and attr_config.get("category") is not None
108 ):
109 existing.category = attr_config.get("category")
110 updated_count += 1
111 continue
113 attribute = RoleAttributes(
114 name=attr_config["name"],
115 group=attr_config.get("group"),
116 category=attr_config.get("category"),
117 )
118 db.add(attribute)
119 created_count += 1
121 await db.commit()
123 except Exception as e:
124 logger.error(f"Failed to create role attributes: {str(e)}")
125 await db.rollback()
126 raise
129async def create_default_roles():
130 """Create system super-admin role and a basic user role."""
131 async with AsyncSessionLocal() as db:
132 try:
133 default_roles = [
134 {
135 "name": settings.DEFAULT_SUPER_ADMIN_ROLE,
136 "description": "System super administrator (full access bypass)",
137 "level": settings.DEFAULT_SUPER_ADMIN_LEVEL,
138 },
139 {
140 "name": "user",
141 "description": "Regular user role with basic permissions",
142 "level": settings.DEFAULT_USER_ROLE_LEVEL,
143 },
144 ]
146 created_count = 0
148 for role_config in default_roles:
149 existing_role = await db.execute(
150 select(Roles).where(Roles.name == role_config["name"])
151 )
152 if existing_role.scalar_one_or_none():
153 continue
155 role = Roles(
156 name=role_config["name"],
157 description=role_config["description"],
158 level=role_config["level"],
159 )
160 db.add(role)
161 created_count += 1
163 await db.commit()
165 except Exception as e:
166 logger.error(f"Failed to create roles: {str(e)}")
167 await db.rollback()
168 raise
171async def create_default_admin():
172 """Create default super-admin account from ENV settings."""
173 async with AsyncSessionLocal() as db:
174 try:
175 super_role_result = await db.execute(
176 select(Roles).where(Roles.name == settings.DEFAULT_SUPER_ADMIN_ROLE)
177 )
178 super_role = super_role_result.scalar_one_or_none()
180 if not super_role:
181 logger.error("Super admin role not found, please run role initialization first")
182 return
184 super_users_result = await db.execute(
185 select(Users)
186 .join(RoleMapper, Users.id == RoleMapper.user_id)
187 .join(Roles, RoleMapper.role_id == Roles.id)
188 .where(Roles.name == settings.DEFAULT_SUPER_ADMIN_ROLE)
189 )
190 existing_super_users = super_users_result.scalars().all()
192 if existing_super_users:
193 logger.info(
194 "Super admin users already exist: "
195 f"{[user.email for user in existing_super_users]}"
196 )
197 await db.commit()
198 return
200 existing_user_result = await db.execute(
201 select(Users).where(Users.email == settings.DEFAULT_ADMIN_EMAIL)
202 )
203 existing_user = existing_user_result.scalar_one_or_none()
205 if existing_user:
206 existing_role_mapping = await db.execute(
207 select(RoleMapper).where(
208 RoleMapper.user_id == existing_user.id, RoleMapper.role_id == super_role.id
209 )
210 )
211 if not existing_role_mapping.scalar_one_or_none():
212 role_mapping = RoleMapper(user_id=existing_user.id, role_id=super_role.id)
213 db.add(role_mapping)
214 logger.info(
215 f"Assigned super admin role to existing user: {existing_user.email}"
216 )
217 await db.execute(
218 delete(RoleMapper).where(
219 RoleMapper.user_id == existing_user.id,
220 RoleMapper.role_id != super_role.id,
221 )
222 )
223 else:
224 admin_user = Users(
225 first_name=settings.DEFAULT_ADMIN_FIRST_NAME,
226 last_name=settings.DEFAULT_ADMIN_LAST_NAME,
227 email=settings.DEFAULT_ADMIN_EMAIL,
228 phone=settings.DEFAULT_ADMIN_PHONE,
229 hash_password=await hash_password(settings.DEFAULT_ADMIN_PASSWORD),
230 status=True,
231 password_reset_required=False,
232 email_verified=True,
233 )
235 db.add(admin_user)
236 await db.commit()
237 await db.refresh(admin_user)
239 role_mapping = RoleMapper(user_id=admin_user.id, role_id=super_role.id)
240 db.add(role_mapping)
242 await db.commit()
243 logger.info("Admin account initialization completed")
245 except Exception as e:
246 logger.error(f"Failed to create admin account: {str(e)}")
247 await db.rollback()
248 raise