1 Commits
0.2.5 ... 0.3.0

Author SHA1 Message Date
8f398c35df Auth / Author Flow Hardening and Client Separation (#1)
All checks were successful
continuous-integration/drone/tag Build is passing
# Merge Request: Auth / Author Flow Hardening and Client Separation

## Summary
This change set improves the authentication–author lifecycle by clearly separating **Auth** and **Blog API** clients, ensuring an **Author is created at registration time**, and preventing user-controlled mutation of immutable identity fields in the UI.

The result is a cleaner contract between services, fewer edge cases around missing authors, and more predictable client behavior.

---

## Key Changes

### 1. Username Made Read-Only in Profile UI
- Disabled the `username` field in `Profile.tsx`
- Prevents accidental or malicious mutation of identity-bound fields
- Aligns UI behavior with backend ownership rules

---

### 2. Dedicated Auth vs Blog API Clients
- Introduced a separate Axios client for the Auth service (`auth`)
- Blog service continues to use `api`
- Both clients:
  - Automatically attach JWT tokens
  - Share centralized `401` handling and token invalidation logic

**Why:**
Auth and Blog are separate concerns and potentially separate services. Explicit clients reduce coupling and eliminate ambiguous routing.

---

### 3. Registration Flow Now Creates Author Automatically
- `register()` now:
  1. Registers the user via Auth service
  2. Creates a corresponding Author via Blog API

This guarantees:
- Every authenticated user has an Author record
- No race condition or implicit author creation later

---

### 4. Correct Endpoint Usage for “Current User”
- `/auth/me` is now correctly called via the Auth client
- `/authors/me` replaces ID-based lookup for the current author
- Eliminates dependency on user ID leaking across service boundaries

---

### 5. Centralized Token & Auth Error Handling
- Shared request interceptor to attach JWT tokens
- Shared response interceptor to handle `401` consistently
- Token invalidation is now uniform across services

---

### 6. Environment Configuration Updated
- Added `VITE_AUTH_BASE_URL` to support separate Auth service routing
- Explicit environment contract avoids accidental misconfiguration

---

## Impact
- Cleaner service boundaries
- Deterministic user → author lifecycle
- Reduced client-side complexity and edge cases
- More secure handling of identity fields

---

## Notes / Follow-ups
- Optional auto-login after registration is scaffolded but commented
- Logout or redirect handling on `401` can be wired later via an event bus or global handler

---

**Risk Level:** Low
**Behavioral Change:** Yes (author auto-created on registration)
**Backward Compatibility:** Requires Auth + Blog services to be reachable separately

Reviewed-on: #1
Co-authored-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
Co-committed-by: Vishesh 'ironeagle' Bangotra <aetoskia@gmail.com>
2025-12-13 13:15:20 +00:00
7 changed files with 59 additions and 27 deletions

View File

@@ -66,6 +66,8 @@ steps:
environment:
API_BASE_URL:
from_secret: API_BASE_URL
AUTH_BASE_URL:
from_secret: AUTH_BASE_URL
volumes:
- name: dockersock
path: /var/run/docker.sock
@@ -76,6 +78,7 @@ steps:
- |
docker build --network=host \
--build-arg VITE_API_BASE_URL="$API_BASE_URL" \
--build-arg VITE_AUTH_BASE_URL="$AUTH_BASE_URL" \
-t apps/blog:$IMAGE_TAG \
-t apps/blog:latest \
/drone/src

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "aetoskia-blog-app",
"version": "0.2.1",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {

View File

@@ -1,6 +1,6 @@
{
"name": "aetoskia-blog-app",
"version": "0.2.5",
"version": "0.3.0",
"private": true,
"scripts": {
"dev": "vite",

View File

@@ -133,6 +133,7 @@ export default function Profile({
label="Username"
name="username"
margin="normal"
disabled={true}
value={formData.username}
onChange={handleChange}
/>

View File

@@ -1,5 +1,5 @@
import React, { createContext, useState, useEffect, useContext } from 'react';
import { api } from '../utils/api';
import { api, auth } from '../utils/api';
import { AuthorModel } from '../types/models';
import { AuthContextModel } from '../types/contexts';
@@ -18,7 +18,14 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setLoading(true);
setError(null);
const res = await api.post('/auth/register', { username, password });
const res = await auth.post('/register', { username, password });
// auto-login
// await login(username, password);
// now create author
await api.post('/authors', { name: null, avatar: null });
return res.data;
} catch (err: any) {
console.error('Registration failed:', err);
@@ -34,7 +41,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setLoading(true);
setError(null);
const res = await api.post('/auth/login', { username, password });
const res = await auth.post('/login', { username, password });
const { access_token, user } = res.data;
if (access_token) {
@@ -99,9 +106,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const fetchCurrentUser = async () => {
if (!token) return;
try {
const me = await api.get<{ _id: string; username: string; email: string }>('/auth/me');
const me = await auth.get('/me');
const author = await api.get<AuthorModel>(`/authors/${me.data._id}`);
const author = await api.get<AuthorModel>(`/authors/me`);
const fullUser = { ...me.data, ...author.data };

View File

@@ -1,8 +1,42 @@
// src/utils/api.ts
import axios from 'axios';
const AUTH_BASE = import.meta.env.VITE_AUTH_BASE_URL;
const API_BASE = import.meta.env.VITE_API_BASE_URL;
//------------------------------------------------------
// COMMON TOKEN ATTACHMENT LOGIC
//------------------------------------------------------
const attachToken = (config: any) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
};
const handleAuthError = (error: any) => {
if (error.response?.status === 401) {
console.warn('Token expired or invalid. Logging out...');
localStorage.removeItem('token');
// Optional: eventBus, redirect, logout callback
}
return Promise.reject(error);
};
//------------------------------------------------------
// AUTH SERVICE CLIENT
//------------------------------------------------------
export const auth = axios.create({
baseURL: AUTH_BASE,
headers: {
'Content-Type': 'application/json',
},
});
//------------------------------------------------------
// BLOG SERVICE CLIENT
//------------------------------------------------------
export const api = axios.create({
baseURL: API_BASE,
headers: {
@@ -10,24 +44,10 @@ export const api = axios.create({
},
});
// 🔹 Attach token from localStorage before each request
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Attach token + 401 handling
api.interceptors.request.use(attachToken);
api.interceptors.response.use((res) => res, handleAuthError);
// 🔹 Handle expired or invalid tokens globally
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
console.warn('Token expired or invalid. Logging out...');
localStorage.removeItem('token');
// Optionally: trigger a redirect or event
}
return Promise.reject(error);
}
);
// Auth service ALSO needs token for /me, /logout, /introspect
auth.interceptors.request.use(attachToken);
auth.interceptors.response.use((res) => res, handleAuthError);

1
src/vite-env.d.ts vendored
View File

@@ -2,6 +2,7 @@
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_AUTH_BASE_URL: string;
}
interface ImportMeta {