feat: Armarium v1.1.0 — dashboard, auth, 2FA, SMTP, settings, deploy
Dashboard: - ApexCharts bar chart (income vs fixed costs vs expenses) and donut chart - KPI cards: income, fixed costs, savings rate with configurable goal - Greeting with time-of-day and locale-aware date/time display Authentication & security: - Email-based login (no username), case-insensitive lookup - JWT access/refresh tokens with rotation and blacklist - TOTP 2FA with QR code, backup codes (copy + PDF export) - 2FA recovery via email code - Cloudflare Turnstile CAPTCHA on login and register Email flows: - Email verification on registration (24h token) - Password reset flow (15min token, anti-enumeration) - Brevo SMTP integration with HTML + plaintext email templates - Notification emails: 2FA recovery, password changed, email changed Settings page: - 2FA management (enable/disable, QR, backup codes) - Active sessions list with per-device revoke - Data export: ZIP with 6 PDFs via fpdf2 - Notification preferences (3 toggles) - Danger zone: account deletion with mandatory export + confirmation phrase UI & layout: - Sidebar with collapsible/flyout mode, Angular signal-based dropdowns - Dark mode (class-based), language switcher (DE/FR/IT/EN) - Mobile-responsive layout with touch-friendly targets - Roboto font via @fontsource (GDPR-compliant, no Google CDN) - Pure Tailwind CSS v3 Infrastructure: - Forgejo Actions CI/CD pipeline (auto-deploy on push to main) - Gunicorn + Nginx + PostgreSQL production setup - Rate limiting, HSTS, secure cookies, CSRF protection
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-08 17:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('account_type', models.CharField(choices=[('asset', 'Asset Account (Bank/Cash)'), ('expense', 'Expense Account (Laden/Empfänger)'), ('revenue', 'Revenue Account (Einnahmequelle)')], default='asset', max_length=20)),
|
||||
('balance', models.DecimalField(decimal_places=2, default=0.0, max_digits=12)),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-08 17:11
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('description', models.CharField(max_length=255)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('date', models.DateField()),
|
||||
('destination_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='deposits', to='finance.account')),
|
||||
('source_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='withdrawals', to='finance.account')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-15 16:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0002_transaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Budget',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('category', models.CharField(choices=[('housing', 'Wohnen'), ('food', 'Lebensmittel'), ('transport', 'Transport'), ('health', 'Gesundheit'), ('entertainment', 'Freizeit'), ('savings', 'Sparen'), ('other', 'Sonstiges')], default='other', max_length=50)),
|
||||
('due_day', models.PositiveSmallIntegerField(help_text='Tag im Monat (1-31)')),
|
||||
('active', models.BooleanField(default=True)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='budgets', to='finance.account')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0003_budget'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='budget',
|
||||
name='due_day',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='budget',
|
||||
old_name='category',
|
||||
new_name='main_category',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='budget',
|
||||
name='main_category',
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
('fixed_expenses', 'Fixe Ausgaben'),
|
||||
('mobile_internet', 'Mobile & Internet'),
|
||||
('leisure', 'Freizeit'),
|
||||
('tax_reserves', 'Steuerrücklagen'),
|
||||
('insurance', 'Versicherungen'),
|
||||
('loans', 'Abzahlungen & Kredite'),
|
||||
],
|
||||
default='fixed_expenses',
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-23 22:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0004_alter_budget'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='budget',
|
||||
name='main_category',
|
||||
field=models.CharField(choices=[('fixed_expenses', 'Fixe Ausgaben'), ('mobile_internet', 'Mobile & Internet'), ('subscriptions', 'Abonnements'), ('leisure', 'Freizeit'), ('tax_reserves', 'Steuerrücklagen'), ('insurance', 'Versicherungen'), ('loans', 'Abzahlungen & Kredite')], default='fixed_expenses', max_length=50),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-23 22:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0005_add_subscriptions_category'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Expense',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('date', models.DateField()),
|
||||
('category', models.CharField(choices=[('groceries', 'Groceries'), ('dining', 'Dining & Restaurants'), ('transport', 'Transport'), ('health', 'Health & Medical'), ('clothing', 'Clothing'), ('electronics', 'Electronics'), ('household', 'Household'), ('entertainment', 'Entertainment'), ('travel', 'Travel'), ('other', 'Other')], default='other', max_length=50)),
|
||||
('notes', models.TextField(blank=True, default='')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses', to='finance.account')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-24 19:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0006_add_expense_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_name', models.CharField(blank=True, default='', max_length=100)),
|
||||
('last_name', models.CharField(blank=True, default='', max_length=100)),
|
||||
('email', models.EmailField(blank=True, default='', max_length=254)),
|
||||
('avatar_color', models.CharField(default='#1A56DB', max_length=7)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-24 19:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0007_add_profile_model'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='avatar_image',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='avatars/'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-24 19:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def assign_legacy_user(apps, schema_editor):
|
||||
User = apps.get_model('auth', 'User')
|
||||
Account = apps.get_model('finance', 'Account')
|
||||
Profile = apps.get_model('finance', 'Profile')
|
||||
|
||||
legacy_user, _ = User.objects.get_or_create(
|
||||
username='legacy_user',
|
||||
defaults={'email': '', 'is_active': True},
|
||||
)
|
||||
|
||||
Account.objects.filter(user__isnull=True).update(user=legacy_user)
|
||||
|
||||
for profile in Profile.objects.filter(user__isnull=True):
|
||||
profile.user = legacy_user
|
||||
profile.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0008_add_avatar_image'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='user',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='user',
|
||||
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.RunPython(assign_legacy_user, migrations.RunPython.noop),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 6.0.3 on 2026-03-24 20:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0009_add_user_fk'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='expense',
|
||||
name='due_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='canton',
|
||||
field=models.CharField(choices=[('AG', 'Aargau'), ('AI', 'Appenzell Innerrhoden'), ('AR', 'Appenzell Ausserrhoden'), ('BE', 'Bern'), ('BL', 'Basel-Landschaft'), ('BS', 'Basel-Stadt'), ('FR', 'Fribourg'), ('GE', 'Geneva'), ('GL', 'Glarus'), ('GR', 'Graubünden'), ('JU', 'Jura'), ('LU', 'Lucerne'), ('NE', 'Neuchâtel'), ('NW', 'Nidwalden'), ('OW', 'Obwalden'), ('SG', 'St. Gallen'), ('SH', 'Schaffhausen'), ('SO', 'Solothurn'), ('SZ', 'Schwyz'), ('TG', 'Thurgau'), ('TI', 'Ticino'), ('UR', 'Uri'), ('VD', 'Vaud'), ('VS', 'Valais'), ('ZG', 'Zug'), ('ZH', 'Zurich')], default='ZH', max_length=2),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Deadline',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('date', models.DateField()),
|
||||
('type', models.CharField(choices=[('tax', 'Tax'), ('insurance', 'Insurance'), ('invoice', 'Invoice'), ('personal', 'Personal'), ('other', 'Other')], default='other', max_length=20)),
|
||||
('notes', models.TextField(blank=True, default='')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='deadlines', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-12 18:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0010_add_calendar_fields'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReadEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_type', models.CharField(choices=[('deadline', 'Deadline'), ('expense', 'Expense')], max_length=20)),
|
||||
('event_id', models.PositiveIntegerField()),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_events', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('user', 'event_type', 'event_id')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.4 on 2026-04-12 18:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0011_readevent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='language',
|
||||
field=models.CharField(choices=[('de', 'Deutsch'), ('fr', 'Français'), ('it', 'Italiano'), ('en', 'English')], default='de', max_length=2),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0012_profile_language'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='totp_secret',
|
||||
field=models.CharField(blank=True, default='', max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='totp_enabled',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,36 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0013_profile_totp'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='totp_last_used_code',
|
||||
field=models.CharField(blank=True, default='', max_length=6),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BackupCode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code_hash', models.CharField(max_length=64)),
|
||||
('used', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='backup_codes',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
)),
|
||||
],
|
||||
options={
|
||||
'indexes': [models.Index(fields=['user', 'used'], name='finance_bac_user_id_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0014_totp_security'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='recovery_email',
|
||||
field=models.EmailField(blank=True, default=''),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0015_profile_recovery_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='recovery_code_hash',
|
||||
field=models.CharField(blank=True, default='', max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='recovery_code_expires',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0016_profile_recovery_code'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='notif_deadlines',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='notif_budget_alerts',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='notif_monthly_summary',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserSession',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('session_key', models.CharField(max_length=64, unique=True)),
|
||||
('refresh_jti', models.CharField(blank=True, default='', max_length=255)),
|
||||
('device_name', models.CharField(blank=True, default='', max_length=200)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('last_active_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name='user_sessions',
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
)),
|
||||
],
|
||||
options={'ordering': ['-last_active_at']},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.0.4 on 2026-05-18 19:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0017_user_sessions_notifications'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameIndex(
|
||||
model_name='backupcode',
|
||||
new_name='finance_bac_user_id_7e357d_idx',
|
||||
old_name='finance_bac_user_id_idx',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='savings_rate_goal',
|
||||
field=models.PositiveSmallIntegerField(default=20),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='canton',
|
||||
field=models.CharField(choices=[('AG', 'Aargau'), ('AI', 'Appenzell Innerrhoden'), ('AR', 'Appenzell Ausserrhoden'), ('BE', 'Bern'), ('BL', 'Basel-Landschaft'), ('BS', 'Basel-Stadt'), ('FR', 'Fribourg'), ('GE', 'Geneva'), ('GL', 'Glarus'), ('GR', 'Graubünden'), ('JU', 'Jura'), ('LU', 'Lucerne'), ('NE', 'Neuchâtel'), ('NW', 'Nidwalden'), ('OW', 'Obwalden'), ('SG', 'St. Gallen'), ('SH', 'Schaffhausen'), ('SO', 'Solothurn'), ('SZ', 'Schwyz'), ('TG', 'Thurgau'), ('TI', 'Ticino'), ('UR', 'Uri'), ('VD', 'Vaud'), ('VS', 'Valais'), ('ZG', 'Zug'), ('ZH', 'Zürich')], default='ZH', max_length=2),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 6.0.4 on 2026-05-19 17:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0018_profile_savings_rate_goal'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='email_verified',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='email_verify_token',
|
||||
field=models.CharField(blank=True, default='', max_length=64),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='password_reset_token_expires',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='password_reset_token_hash',
|
||||
field=models.CharField(blank=True, default='', max_length=64),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0.4 on 2026-05-19 18:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('finance', '0019_profile_email_verification_and_password_reset'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='email_verify_token_expires',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user