Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ALEJ4NDRO2025/urban-store/llms.txt

Use this file to discover all available pages before exploring further.

Urban Store’s authentication system is built from scratch on top of PyJWT and bcrypt — it deliberately avoids DRF SimpleJWT and Django’s built-in session framework. Every API view sets authentication_classes = [] to prevent DRF from intercepting tokens, and all identity checks are performed by decoding the JWT manually. New accounts must verify their email address with a 6-digit code before they can log in, and the verification step enforces rate limits to prevent brute-force attempts.

How JWT Works in Urban Store

Urban Store uses PyJWT directly. Tokens are signed with settings.SECRET_KEY using the HS256 algorithm and carry a custom payload:
ClaimValue
user_idMongoDB ObjectId as a string
emailUser’s email address (also used as the internal user identifier across services)
is_adminBoolean — true for admin accounts, false otherwise
expExpiry timestamp — 7 days from issuance
payload = {
    'user_id': str(user.id),
    'email': user.email,
    'is_admin': user.is_admin,
    'exp': datetime.utcnow() + timedelta(days=7)
}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')

Token Usage

Pass the token as a Bearer token in every authenticated request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
The frontend stores the token in localStorage under the key access and reads it using localStorage.getItem('access').

Registration and Verification Flow

1

Register — POST /api/users/register/

The client posts email, password, first_name, and last_name. The backend creates a new User document with is_verified=False, generates a random 6-digit code, stores it in verification_token (expires in 24 hours), and sends an HTML verification email.
POST /api/users/register/
Content-Type: application/json

{
  "email": "usuario@ejemplo.com",
  "password": "contraseña_segura",
  "first_name": "Alejandro",
  "last_name": "García"
}
Response 201:
{
  "message": "Usuario registrado. Revisa tu correo para obtener el código de verificación.",
  "email": "usuario@ejemplo.com"
}
2

Verify Code — POST /api/users/verify-code/

The user submits their email and the 6-digit code from the email. The backend enforces a maximum of 3 failed attempts. After 3 failures, the account enters a 15-minute lockout. A correct code sets is_verified=True and clears all verification fields.
POST /api/users/verify-code/
Content-Type: application/json

{
  "email": "usuario@ejemplo.com",
  "code": "483920"
}
Response 200:
{
  "message": "Cuenta verificada exitosamente"
}
Response 429 (too many attempts):
{
  "error": "Demasiados intentos fallidos. Intenta de nuevo en 12 minutos o solicita un nuevo código."
}
3

Login — POST /api/users/login/

Verified, active users can now log in. The backend checks the bcrypt password hash and returns a signed JWT.
POST /api/users/login/
Content-Type: application/json

{
  "email": "usuario@ejemplo.com",
  "password": "contraseña_segura"
}
Response 200:
{
  "message": "Login exitoso",
  "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "email": "usuario@ejemplo.com",
  "name": "Alejandro",
  "is_admin": false
}
Users who have not verified their email receive 403 Forbidden on login with the message "Debes verificar tu correo antes de iniciar sesión". Deactivated accounts (is_active=False) also receive 403.

Resending the Verification Code

If the user loses or doesn’t receive the code they can request a new one:
POST /api/users/resend-verification/
Content-Type: application/json

{
  "email": "usuario@ejemplo.com"
}
A 2-minute cooldown is enforced between sends using last_verification_sent_at. Requests within the cooldown window receive a 429 response with the number of seconds remaining. A successful resend generates a new 6-digit code, resets verification_attempts to 0, and extends the expiry by 24 hours.

Password Hashing

All passwords are hashed with bcrypt before storage:
import bcrypt

# On registration
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
user.password = hashed.decode('utf-8')

# On login verification
bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8'))
Passwords are never stored in plain text. The gensalt() call generates a new random salt for every hash, so identical passwords produce different hashes.

Admin Access

The is_admin field on the User document is a boolean set to False by default. There is no API endpoint to promote a user to admin — the flag must be set directly in MongoDB (e.g. via MongoDB Atlas or a mongo shell command).
// MongoDB shell
db.users.updateOne(
  { email: "admin@urbanstore.com" },
  { $set: { is_admin: true } }
)
Admin status is embedded in the JWT payload at login time. The Next.js middleware and the backend IsAdminMongo permission class both read is_admin from the JWT — not from the database — so a re-login is required after promoting a user.

Route Protection — Next.js Middleware

The middleware.js file at the project root intercepts all page navigations and enforces three rules:

Public Paths

Accessible without any token: /, /catalog, /login, /register, /verify-email, /forgot-password, /reset-password

Protected Paths

Any path not in the public list requires a valid access cookie. Unauthenticated requests are redirected to /login?redirect=<original-path>.

Admin-Only Paths

Any path starting with /admin requires the is_admin claim in the JWT to be true. Non-admins are redirected to /.
The middleware reads the token from the access cookie (not localStorage). The frontend must set this cookie in addition to storing the token in localStorage so that the middleware can access it server-side.
// Middleware admin check (client-side JWT decode — no signature verification)
const payload = JSON.parse(atob(token.split('.')[1]))
if (!payload.is_admin) {
  return NextResponse.redirect(new URL('/', request.url))
}
The middleware decodes the JWT on the client side using atob() — it does not cryptographically verify the signature. It is a UX guard, not a security boundary. All security enforcement happens on the Django backend, which verifies the signature with settings.SECRET_KEY.

User Model Fields

The User document is stored in the users MongoDB collection. All fields come from users/models.py:
FieldMongoEngine TypeDescription
emailEmailField(required=True, unique=True)Primary identifier
passwordStringField(required=True)bcrypt hash
first_nameStringField(max_length=30)Given name
last_nameStringField(max_length=30)Family name
is_activeBooleanField(default=True)Soft-delete flag
is_adminBooleanField(default=False)Admin privilege flag
is_verifiedBooleanField(default=False)Email verification status
verification_tokenStringFieldCurrent 6-digit code
verification_token_expiresDateTimeFieldCode expiry (24 h for registration, 1 h for password reset)
created_atDateTimeField(default=datetime.utcnow)Account creation timestamp
last_verification_sent_atDateTimeFieldTimestamp of last code send (for cooldown enforcement)
verification_attemptsIntField(default=0)Failed code attempt counter
last_failed_attempt_atDateTimeFieldTimestamp of last failed attempt (for lockout window)

Soft Delete

Deleting a user account does not remove the document from MongoDB. DELETE /api/users/profile/ sets is_active=False:
user.is_active = False
user.save()
Soft-deleted accounts cannot log in (403 Forbidden). They remain in the database for order history and audit purposes, and can be reactivated by setting is_active=True directly in MongoDB.

Additional Auth Endpoints

MethodEndpointDescription
GET/api/users/profile/Get authenticated user’s profile
PUT/api/users/profile/Update first_name and last_name
DELETE/api/users/profile/Soft-delete account
POST/api/users/change-password/Change password (requires current password)
POST/api/users/forgot-password/Request a password-reset code via email
POST/api/users/reset-password/Confirm reset code and set new password

Build docs developers (and LLMs) love