update erd db and add display name to user access table

This commit is contained in:
Alberto-Audrix committed 2026-09-10 10:50:42 +07:00
1 parent ed84d989b7
commit ae2ea27ae2
13 files changed
+91 -10

No files matched your search

@@ -0,0 +1,25 @@
from django.db import migrations, models
def backfill_display_name(apps, schema_editor):
User = apps.get_model("accounts", "User")
for user in User.objects.all().iterator():
if not (user.display_name or "").strip():
User.objects.filter(pk=user.pk).update(display_name=user.user_name)
class Migration(migrations.Migration):
dependencies = [
("accounts", "0004_sync_status_flags"),
]
operations = [
migrations.AddField(
model_name="user",
name="display_name",
field=models.CharField(default="", max_length=100),
preserve_default=False,
),
migrations.RunPython(backfill_display_name, migrations.RunPython.noop),
]
+2
View File
@@ -13,6 +13,7 @@ class UserManager(BaseUserManager):
if not user_name:
raise ValueError("user_name is required")
extra_fields.setdefault("status", User.STATUS_ACTIVE)
extra_fields.setdefault("display_name", user_name)
user = self.model(user_name=user_name, **extra_fields)
user.set_password(password)
user.save(using=self._db)
@@ -39,6 +40,7 @@ class User(AbstractBaseUser, PermissionsMixin):
user_id = models.BigAutoField(primary_key=True)
user_name = models.CharField(max_length=30, unique=True)
display_name = models.CharField(max_length=100)
status = models.CharField(max_length=30, choices=STATUS_CHOICES, default=STATUS_ACTIVE)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
+2 -2
View File
@@ -15,8 +15,8 @@ class PkAsIdMixin(serializers.Serializer):
class UserSerializer(PkAsIdMixin, serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "user_name", "status", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at"]
fields = ["id", "user_name", "display_name", "status", "created_at", "updated_at"]
read_only_fields = ["id", "user_name", "created_at", "updated_at"]
class LoginSerializer(serializers.Serializer):
@@ -18,6 +18,7 @@ def ensure_bootstrap_user(
user, created = User.objects.get_or_create(
user_name=user_name,
defaults={
"display_name": user_name,
"status": status,
"is_staff": is_staff,
"is_superuser": is_superuser,
@@ -31,6 +32,8 @@ def ensure_bootstrap_user(
)
should_set_password = created or not user.has_usable_password() or force_password
if not (user.display_name or "").strip():
user.display_name = user_name
if should_set_password:
user.set_password(password)
user.status = status
@@ -43,7 +46,9 @@ def ensure_bootstrap_user(
user.status = status
user.is_staff = is_staff
user.is_superuser = is_superuser
user.save(update_fields=["status", "is_staff", "is_superuser", "updated_at"])
user.save(
update_fields=["display_name", "status", "is_staff", "is_superuser", "updated_at"]
)
stdout.write(f"User {user_name} already exists (password unchanged)")
return user
@@ -16,13 +16,23 @@ class Command(BaseCommand):
def handle(self, *args, **options):
user, created = User.objects.get_or_create(
user_name="admin",
defaults={"status": User.STATUS_SUPERADMIN, "is_staff": True, "is_superuser": True},
defaults={
"display_name": "Admin",
"status": User.STATUS_SUPERADMIN,
"is_staff": True,
"is_superuser": True,
},
)
if created or not user.has_usable_password():
user.set_password("admin123")
user.status = User.STATUS_SUPERADMIN
if not (user.display_name or "").strip():
user.display_name = "Admin"
user.save()
self.stdout.write("Created/updated user admin / admin123")
elif not (user.display_name or "").strip():
user.display_name = "Admin"
user.save(update_fields=["display_name", "updated_at"])
raw_key = None
bootstrap = settings.BOOTSTRAP_API_KEY
@@ -178,10 +178,16 @@ class Command(BaseCommand):
user, created = User.objects.get_or_create(
user_name="staff",
defaults={"status": User.STATUS_ACTIVE, "is_staff": True},
defaults={
"display_name": "Staff",
"status": User.STATUS_ACTIVE,
"is_staff": True,
},
)
user.status = User.STATUS_ACTIVE
user.is_staff = True
if not (user.display_name or "").strip():
user.display_name = "Staff"
user.set_password(password)
user.save()
if created:
@@ -65,10 +65,16 @@ class Command(BaseCommand):
user, created = User.objects.get_or_create(
user_name="staff",
defaults={"status": User.STATUS_ACTIVE, "is_staff": True},
defaults={
"display_name": "Staff",
"status": User.STATUS_ACTIVE,
"is_staff": True,
},
)
user.status = User.STATUS_ACTIVE
user.is_staff = True
if not (user.display_name or "").strip():
user.display_name = "Staff"
user.set_password(password)
user.save()
if created:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 KiB

After

Width:  |  Height:  |  Size: 233 KiB

+21
View File
@@ -3,11 +3,13 @@
-- Source: ERD_dashboard.png (updated 2026-09-09 for GM cycle-close flow)
--
-- user_access.status: active | inactive | superadmin
-- user_access.display_name: shown in dashboard UI (login still uses user_name)
-- (Django syncs is_active/is_staff/is_superuser from status on save:
-- inactive => all false; superadmin => is_active/is_superuser true,
-- is_staff false; active => is_active true, is_superuser false, is_staff
-- left untouched so operators can be marked staff)
-- user_access → sites → kandang → cycles
-- user_access → api_keys (X-API-Key auth; hashed at rest)
-- cycles.status: active | pending_close | closed (GM approves end_date)
-- cycles.close_requested_by_id → user_access (optional)
-- kandang → flock → iot_panel
@@ -26,6 +28,7 @@
CREATE TABLE IF NOT EXISTS user_access (
user_id SERIAL PRIMARY KEY,
user_name VARCHAR(30) NOT NULL UNIQUE,
display_name VARCHAR(100) NOT NULL,
password VARCHAR(128) NOT NULL,
last_login TIMESTAMP WITH TIME ZONE,
is_superuser BOOLEAN NOT NULL DEFAULT FALSE,
@@ -51,6 +54,24 @@ CREATE TABLE IF NOT EXISTS user_access_user_permissions (
permission_id INTEGER NOT NULL REFERENCES auth_permission (id) ON DELETE CASCADE
);
-- -----------------------------------------------------------------------------
-- 1b. API Keys (machine auth via X-API-Key; FK → user_access)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES user_access (user_id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
prefix VARCHAR(12) NOT NULL,
key_hash VARCHAR(64) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
last_used_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys (user_id);
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys (prefix);
-- -----------------------------------------------------------------------------
-- 2. Sites
-- -----------------------------------------------------------------------------
+7 -3
View File
@@ -129,10 +129,12 @@ const Header: React.FC<HeaderProps> = ({
className="flex items-center space-x-2 focus:outline-none"
>
<div className="w-10 h-10 rounded-full bg-red-100 text-red-700 flex items-center justify-center font-bold">
{user.user_name.slice(0, 1).toUpperCase()}
{(user.display_name || user.user_name).slice(0, 1).toUpperCase()}
</div>
<div className="hidden lg:block text-left">
<p className="text-sm font-semibold text-gray-800">{user.user_name}</p>
<p className="text-sm font-semibold text-gray-800">
{user.display_name || user.user_name}
</p>
<p className="text-xs text-gray-500">
{user.status === 'superadmin' ? 'Admin' : 'Pengguna'}
</p>
@@ -142,7 +144,9 @@ const Header: React.FC<HeaderProps> = ({
{open && (
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 z-50">
<div className="px-4 py-3 border-b">
<p className="text-sm font-semibold text-gray-800">{user.user_name}</p>
<p className="text-sm font-semibold text-gray-800">
{user.display_name || user.user_name}
</p>
<p className="text-xs text-gray-500">
{user.status === 'superadmin' ? 'Admin' : 'Pengguna'}
</p>
@@ -62,6 +62,7 @@ vi.mock('../../context/AuthContext.tsx', () => ({
user: {
id: 99,
user_name: 'tester',
display_name: 'Tester',
status: 'superadmin',
created_at: '',
updated_at: '',
+1 -1
View File
@@ -42,7 +42,7 @@ describe('apiClient', () => {
it('sends CSRF token on POST', async () => {
document.cookie = 'csrftoken=abc123; path=/';
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1, user_name: 'admin' }));
const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ id: 1, user_name: 'admin', display_name: 'Admin' }));
vi.stubGlobal('fetch', fetchMock);
await api.auth.login('admin', 'admin123');
+1
View File
@@ -10,6 +10,7 @@ export type UserStatus = 'active' | 'inactive' | 'superadmin';
export type User = {
id: number;
user_name: string;
display_name: string;
status: UserStatus;
created_at: string;
updated_at: string;