Skip to content

Commit ea976e4

Browse files
Merge pull request #102 from pixie-git/feature/PIX-93_change-username
PIX-93: change username in profileview
2 parents 7eb4162 + 04e5209 commit ea976e4

6 files changed

Lines changed: 185 additions & 9 deletions

File tree

client/src/services/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ api.interceptors.request.use((config) => {
2020
return config
2121
})
2222

23+
export const updateUser = (userId: string, username: string, config?: AxiosRequestConfig) =>
24+
api.put<{ username: string, token: string }>(`/users/${userId}`, { username }, config);
25+
2326
import { useToastStore } from "../stores/toast.store";
2427

2528
// Global Error Handling

client/src/stores/user.store.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ export const useUserStore = defineStore("user", () => {
1616
localStorage.setItem("authToken", authToken)
1717
localStorage.setItem("userId", userId)
1818
localStorage.setItem("isAdmin", String(admin))
19+
20+
}
21+
22+
async function updateProfile(newUsername: string) {
23+
if (!id.value) return;
24+
// Import dynamically to avoid circular dependency if any, or move import to top if safe
25+
const api = await import('../services/api');
26+
27+
const response = await api.updateUser(id.value, newUsername);
28+
const { username: name, token: newToken } = response.data;
29+
30+
username.value = name;
31+
token.value = newToken;
32+
localStorage.setItem("username", name);
33+
localStorage.setItem("authToken", newToken);
1934
}
2035

2136
function logout(): void {
@@ -31,5 +46,5 @@ export const useUserStore = defineStore("user", () => {
3146

3247
const isAuthenticated = computed(() => !!token.value)
3348

34-
return { username, token, id, isAdmin, isAuthenticated, login, logout }
49+
return { username, token, id, isAdmin, isAuthenticated, login, logout, updateProfile }
3550
})

client/src/views/ProfileView.vue

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,24 @@
1111

1212
<div class="form-group">
1313
<label>Username:</label>
14-
<input type="text" :value="userStore.username" disabled class="input-field" />
14+
<div class="username-edit-group">
15+
<input
16+
type="text"
17+
v-model="editUsername"
18+
:disabled="!isEditing"
19+
class="input-field"
20+
:class="{ 'editing': isEditing }"
21+
/>
22+
<div class="edit-actions">
23+
<button v-if="!isEditing" @click="startEditing" class="edit-btn">Edit</button>
24+
<div v-else class="action-buttons">
25+
<button @click="saveUsername" class="save-btn" :disabled="isSaving">
26+
{{ isSaving ? 'Saving...' : 'Save' }}
27+
</button>
28+
<button @click="cancelEditing" class="cancel-btn" :disabled="isSaving">Cancel</button>
29+
</div>
30+
</div>
31+
</div>
1532
</div>
1633

1734
<button class="logout-btn" @click="handleLogout">
@@ -64,6 +81,43 @@ const router = useRouter();
6481
const userStore = useUserStore();
6582
const notificationStore = useInAppNotificationStore();
6683
const { isMuted } = storeToRefs(notificationStore);
84+
import { useToastStore } from '../stores/toast.store';
85+
86+
const toastStore = useToastStore();
87+
const isEditing = ref(false);
88+
const isSaving = ref(false);
89+
const editUsername = ref(userStore.username);
90+
91+
const startEditing = () => {
92+
editUsername.value = userStore.username;
93+
isEditing.value = true;
94+
};
95+
96+
const cancelEditing = () => {
97+
isEditing.value = false;
98+
editUsername.value = userStore.username;
99+
};
100+
101+
const saveUsername = async () => {
102+
if (!editUsername.value.trim() || editUsername.value === userStore.username) {
103+
isEditing.value = false;
104+
return;
105+
}
106+
107+
try {
108+
isSaving.value = true;
109+
await userStore.updateProfile(editUsername.value);
110+
toastStore.add('Username updated successfully', 'success');
111+
isEditing.value = false;
112+
} catch (error: any) {
113+
// Show specific error feedback
114+
const errorMessage = error.response?.data?.error || "Failed to update username";
115+
toastStore.add(errorMessage, 'error');
116+
console.error("Failed to update username", error);
117+
} finally {
118+
isSaving.value = false;
119+
}
120+
};
67121
68122
const currentTheme = ref('light');
69123
@@ -267,4 +321,47 @@ const toggleNotifications = () => {
267321
padding: 1.5rem;
268322
}
269323
}
324+
325+
/* Edit Styles */
326+
.username-edit-group {
327+
display: flex;
328+
gap: 1rem;
329+
align-items: center;
330+
}
331+
332+
.edit-actions {
333+
display: flex;
334+
align-items: center;
335+
}
336+
337+
.action-buttons {
338+
display: flex;
339+
gap: 0.5rem;
340+
}
341+
342+
.edit-btn, .cancel-btn {
343+
padding: 0.5rem 1rem;
344+
background: transparent;
345+
border: 1px solid var(--color-border);
346+
border-radius: 4px;
347+
color: var(--color-text);
348+
cursor: pointer;
349+
font-size: 0.85rem;
350+
}
351+
352+
.save-btn {
353+
padding: 0.5rem 1rem;
354+
background: #10b981;
355+
border: none;
356+
border-radius: 4px;
357+
color: white;
358+
cursor: pointer;
359+
font-size: 0.85rem;
360+
}
361+
362+
.save-btn:disabled {
363+
opacity: 0.7;
364+
cursor: not-allowed;
365+
}
366+
270367
</style>

server/src/controllers/user.controller.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Request, Response, NextFunction } from "express"
22
import { UserService } from "../services/user.service.js"
3+
import { AppError } from "../utils/AppError.js"
34

45
export class UserController {
56
static async getAll(req: Request, res: Response, next: NextFunction): Promise<void> {
@@ -11,4 +12,32 @@ export class UserController {
1112
next(err)
1213
}
1314
}
15+
16+
static async update(req: Request, res: Response, next: NextFunction): Promise<void> {
17+
try {
18+
const { id } = req.params
19+
const { username } = req.body
20+
const authUser = (req as any).user
21+
22+
if (!authUser || authUser.id !== id) {
23+
throw new AppError("Unauthorized", 403)
24+
}
25+
26+
if (!username || username.trim().length === 0) {
27+
throw new AppError("Username is required", 400)
28+
}
29+
30+
try {
31+
const result = await UserService.updateUser(id, username)
32+
res.json(result)
33+
} catch (err: any) {
34+
if (err.message === "Username already taken") {
35+
throw new AppError("Username already taken", 409)
36+
}
37+
throw err
38+
}
39+
} catch (err) {
40+
next(err)
41+
}
42+
}
1443
}

server/src/routes/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ router.post("/login", LoginController.login)
1313

1414
// Users
1515
router.get("/users", authenticateToken, UserController.getAll)
16+
router.put("/users/:id", authenticateToken, UserController.update)
1617

1718
// Test Error
1819

server/src/services/user.service.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ interface LoginResponse {
1010
isAdmin: boolean
1111
}
1212

13+
interface UpdateUserResponse {
14+
username: string
15+
token: string
16+
}
17+
1318
interface AllUsersResponse {
1419
username: string
1520
isAdmin: boolean
1621
_id?: string
1722
}
1823

1924
export class UserService {
20-
/**
21-
* Handle user login/registration and token generation
22-
*/
2325
static async login(username: string): Promise<LoginResponse> {
2426
const JWT_SECRET = CONFIG.JWT.SECRET
2527

@@ -31,7 +33,8 @@ export class UserService {
3133
isNewUser = true
3234
}
3335

34-
const token = jwt.sign({ id: user._id, username: user.username, isAdmin: user.isAdmin }, JWT_SECRET, { expiresIn: CONFIG.JWT.EXPIRES_IN as any })
36+
37+
const token = this.generateToken(user)
3538

3639
return {
3740
username: user.username,
@@ -42,9 +45,6 @@ export class UserService {
4245
}
4346
}
4447

45-
/**
46-
* Retrieve all users with limited fields
47-
*/
4848
static async getAllUsers(): Promise<AllUsersResponse[]> {
4949
const users = await User.find({}, "username isAdmin")
5050
return users.map((user) => ({
@@ -53,4 +53,35 @@ export class UserService {
5353
_id: user._id?.toString(),
5454
}))
5555
}
56+
57+
static async updateUser(userId: string, newUsername: string): Promise<UpdateUserResponse> {
58+
const JWT_SECRET = CONFIG.JWT.SECRET
59+
60+
const existing = await User.findOne({ username: newUsername })
61+
if (existing && existing._id.toString() !== userId) {
62+
throw new Error("Username already taken")
63+
}
64+
65+
const user = await User.findByIdAndUpdate(userId, { username: newUsername }, { new: true })
66+
if (!user) {
67+
throw new Error("User not found")
68+
}
69+
70+
const token = this.generateToken(user)
71+
72+
return {
73+
username: user.username,
74+
token
75+
}
76+
}
77+
78+
private static generateToken(user: any): string {
79+
const JWT_SECRET = CONFIG.JWT.SECRET
80+
return jwt.sign(
81+
{ id: user._id, username: user.username, isAdmin: user.isAdmin },
82+
JWT_SECRET,
83+
{ expiresIn: CONFIG.JWT.EXPIRES_IN as any }
84+
)
85+
}
5686
}
87+

0 commit comments

Comments
 (0)