A production-quality AI-powered business platform inspired by Polsia, built with Flutter Web (frontend), Django REST Framework (backend), and PostgreSQL (database).
-
JWT Authentication System
- Registration endpoint with email validation
- Login endpoint with secure password verification
- Token refresh endpoint with blacklisting
- Logout endpoint that invalidates refresh tokens
- Current user endpoint (
GET /api/auth/me/) - Profile update endpoint (
PATCH /api/auth/me/) - Password change endpoint (
POST /api/auth/change-password/)
-
Secure Token Management
- Short-lived access tokens (15 minutes)
- Long-lived refresh tokens (7 days)
- Refresh token rotation on each use
- Blacklisting after logout or rotation
- Tokens stored securely in Flutter using
shared_preferences
-
Company Ownership Enforcement
POST /api/companies/- owner automatically set to authenticated userGET /api/companies/- returns only the current user's companiesGET /api/companies/{id}/- returns 404 if company doesn't belong to userPATCH /api/companies/{id}/- only owner can updateDELETE /api/companies/{id}/- only owner can deleteownerfield removed from client input entirely
-
Flutter Authentication UI
- Login screen with email/password form
- Register screen with name/email/password form
- Password visibility toggle
- Form validation with user-friendly error messages
- Loading states during authentication
- Centralized auth state management with
AuthProvider - Token storage with
TokenStorage
-
Protected API Flow
- All company endpoints require authentication
- Unauthenticated requests return 401
- Access token sent automatically in
Authorization: Bearerheader - 401 responses trigger token refresh or logout
backend/config/settings.py- Added token blacklist app, adjusted JWT settingsbackend/accounts/serializers.py- Added RegisterSerializer, LoginSerializer, ChangePasswordSerializerbackend/accounts/views.py- Added register, login, logout, me, change-password viewsbackend/accounts/urls.py- Added auth endpoint URLsbackend/companies/serializers.py- Made owner read-onlybackend/companies/views.py- Enforced ownership, removed AllowAny permission
frontend/lib/models/user.dart- User model with JSON serializationfrontend/lib/storage/token_storage.dart- Secure token storage using shared_preferencesfrontend/lib/services/auth_service.dart- Authentication service (register, login, logout, refresh, change password)frontend/lib/services/api_service.dart- Updated to support optional accessToken parameterfrontend/lib/providers/auth_provider.dart- Centralized auth state managementfrontend/lib/screens/auth/login_screen.dart- Login UIfrontend/lib/screens/auth/register_screen.dart- Registration UIfrontend/lib/main.dart- Updated with auth flow and ChangeNotifierProviderfrontend/lib/screens/company_test_screen.dart- Updated to use auth tokensfrontend/pubspec.yaml- Addedproviderandshared_preferencesdependencies
POST /api/auth/register/ - Create account
POST /api/auth/login/ - Login and get tokens
POST /api/auth/logout/ - Invalidate refresh token
POST /api/auth/token/refresh/ - Get new access token
GET /api/auth/me/ - Get current user
PATCH /api/auth/me/ - Update profile
POST /api/auth/change-password/ - Change password
What authentication means: Authentication is the process of verifying who you are. When you log in with email and password, Django checks if those credentials are correct. If they are, Django gives you a special "key" (JWT token) that proves you're authenticated.
What JWT is: JWT (JSON Web Token) is a compact, secure way to transmit information between parties. In our case, it's a signed token that contains the user's ID and expiration time. The frontend stores this token and sends it with every API request.
What an access token is:
An access token is a short-lived key (15 minutes) that proves you're authenticated. You send it in the Authorization: Bearer <token> header with every API request. Django verifies the token and knows who you are.
What a refresh token is:
A refresh token is a long-lived key (7 days) that can be used to get a new access token when the old one expires. It's stored securely and only sent to the /api/auth/token/refresh/ endpoint.
How Flutter sends the token to Django:
Every API request includes an Authorization: Bearer <access_token> header. The ApiService methods accept an optional accessToken parameter. The AuthProvider provides the current token.
How Django identifies the logged-in user:
When a request comes in with a JWT token, Django's JWTAuthentication class decodes the token, extracts the user ID, and sets request.user to that user. All subsequent code can access the authenticated user via request.user.
How Django protects company data:
The CompanyViewSet now:
- Requires authentication (
IsAuthenticatedpermission) - Filters the queryset to only return companies where
owner = request.user - Automatically sets
owner = request.userwhen creating a company - Returns 404 if a user tries to access a company they don't own
Why users cannot simply send another user's ID:
The backend ignores any owner field sent by the client. Instead, it uses request.user (extracted from the JWT token) as the owner. Even if a malicious user sends "owner": 123, Django overwrites it with the authenticated user's ID.
How password hashing works: When a user registers or changes their password, Django doesn't store the plain-text password. Instead, it uses a one-way hashing algorithm (PBKDF2 with SHA256). The password is transformed into a hash that cannot be reversed. When the user logs in, Django hashes the entered password and compares it to the stored hash.
What happens when a token expires:
- The access token expires after 15 minutes
- Django returns a 401 Unauthorized response
- Flutter attempts to refresh using the refresh token
- If refresh succeeds, the new access token is stored and the original request is retried
- If refresh fails (token expired/blacklisted), Flutter clears auth state and shows the login screen
How logout works with JWT: Since JWT is stateless, we can't "delete" the access token. Instead, we:
- Blacklist the refresh token in the database
- Clear tokens from Flutter's local storage
- The access token will naturally expire within 15 minutes
- The refresh token is permanently invalid
Start-Service postgresql-x64-16$env:PATH = "C:\Users\acer\AppData\Local\Programs\Python\Python312;C:\Users\acer\AppData\Local\Programs\Python\Python312\Scripts;" + $env:PATH
cd C:\Users\acer\Downloads\bizorai\backend
python manage.py runserver 0.0.0.0:8000$env:PATH = "C:\Users\acer\AppData\Local\Programs\Python\Python312;C:\Users\acer\AppData\Local\Programs\Python\Python312\Scripts;" + $env:PATH
cd C:\Users\acer\Downloads\bizorai\frontend
flutter run -d chrome --web-port 8080Visit http://localhost:8080 in Chrome.
$body = @{
name = "Test User"
email = "testuser@example.com"
password = "SecurePassword123"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8000/api/auth/register/" -Method Post -ContentType "application/json" -Body $body$body = @{
email = "testuser@example.com"
password = "SecurePassword123"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8000/api/auth/login/" -Method Post -ContentType "application/json" -Body $body$token = "YOUR_ACCESS_TOKEN"
$headers = @{Authorization = "Bearer $token"; "Content-Type" = "application/json"}
$body = @{
name = "My Bookstore"
description = "An online bookstore"
industry = "Education"
target_customer = "Students"
business_model = "E-commerce"
goals = "Provide affordable books"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8000/api/companies/" -Method Post -Headers $headers -Body $body$headers = @{Authorization = "Bearer $token"}
Invoke-RestMethod -Uri "http://localhost:8000/api/companies/" -Method Get -Headers $headers$headers = @{Authorization = "Bearer $token"}
Invoke-RestMethod -Uri "http://localhost:8000/api/companies/ANOTHER_USERS_COMPANY_ID/" -Method Get -Headers $headers$body = @{
current_password = "SecurePassword123"
new_password = "NewPassword456"
} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8000/api/auth/change-password/" -Method Post -Headers $headers -Body $body$body = @{refresh = "YOUR_REFRESH_TOKEN"} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8000/api/auth/logout/" -Method Post -Headers $headers -Body $body-
"User with this email address already exists."
- The email is already registered. Use a different email or login instead.
-
"Invalid email or password."
- Check that the email and password are correct. This message is generic for security.
-
"Token is invalid" or 401 Unauthorized
- The access token has expired. The app should automatically refresh it.
- If refresh also fails, you need to login again.
-
"No Company matches the given query."
- The company either doesn't exist or doesn't belong to the authenticated user.
-
CORS errors in Flutter
- Make sure the Flutter dev server port (8080) is in Django's
CORS_ALLOWED_ORIGINS.
- Make sure the Flutter dev server port (8080) is in Django's
- Passwords are hashed using Django's PBKDF2 algorithm - never stored in plain text
- JWT tokens are short-lived (15 min access, 7 day refresh)
- Refresh tokens are rotated and blacklisted on logout
- Owner field is never trusted from the client - always uses
request.user - CORS is configured for specific local origins only
- No sensitive data is exposed in API responses (no passwords, no hashes)
- Generic error messages are used for login failures to prevent user enumeration
-
What authentication means: Verifying a user's identity (who they claim to be) using credentials like email and password.
-
What JWT is: A secure, compact token that contains user information and is signed so it can't be tampered with.
-
What an access token is: A short-lived key that proves you're logged in and allows you to access protected APIs.
-
What a refresh token is: A long-lived key used to get new access tokens without requiring the user to log in again.
-
How Flutter sends the token to Django: By including
Authorization: Bearer <token>in the HTTP request headers. -
How Django identifies the logged-in user: By decoding the JWT token and extracting the user ID, then setting
request.user. -
How Django protects company data: By filtering the queryset to only include companies owned by the authenticated user.
-
Why users cannot simply send another user's ID: Because the backend ignores the
ownerfield from the client and usesrequest.userinstead. -
How password hashing works: Django transforms passwords into irreversible hashes using PBKDF2, so even if the database is compromised, passwords can't be recovered.
-
What happens when a token expires: Django returns 401, Flutter tries to refresh with the refresh token, or redirects to login if refresh fails.
-
How logout works with JWT: The refresh token is blacklisted in the database, and local tokens are cleared from the device.
- Registration endpoint with validation
- Login endpoint with JWT tokens
- Token refresh endpoint
- Logout with token blacklisting
- Current user endpoint
- Profile update endpoint
- Password change endpoint
- Company ownership enforcement
- Owner field removed from client input
- Flutter auth screens (login/register)
- Flutter token storage
- Flutter auth state management
- End-to-end testing verified
Phase 3 Status: COMPLETE
- JWT authentication fully functional
- Company ownership enforced server-side
- Flutter auth flow working
- Data persists in PostgreSQL