Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 259f00e7a0 | |||
| e9f2760815 | |||
| d0e2ee0783 | |||
| c620146c4b |
@@ -18,7 +18,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/Backend_Tests-564%2F564-brightgreen?logo=vitest" alt="Backend Tests 454/454" />
|
<img src="https://img.shields.io/badge/Backend_Tests-569%2F569-brightgreen?logo=vitest" alt="Backend Tests 454/454" />
|
||||||
<img src="https://img.shields.io/badge/Frontend_Tests-777%2F777-brightgreen?logo=vitest" alt="Frontend Tests 611/611" />
|
<img src="https://img.shields.io/badge/Frontend_Tests-777%2F777-brightgreen?logo=vitest" alt="Frontend Tests 611/611" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "medassist-ng-backend",
|
"name": "medassist-ng-backend",
|
||||||
"version": "1.14.4",
|
"version": "1.15.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const sensitiveRateLimitConfig = {
|
|||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
|
.trim()
|
||||||
.min(3, "Username must be at least 3 characters")
|
.min(3, "Username must be at least 3 characters")
|
||||||
.max(50, "Username must be at most 50 characters")
|
.max(50, "Username must be at most 50 characters")
|
||||||
.regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"),
|
.regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"),
|
||||||
@@ -63,7 +64,7 @@ const registerSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
username: z.string().min(1, "Username is required"),
|
username: z.string().trim().min(1, "Username is required"),
|
||||||
password: z.string().min(1, "Password is required"),
|
password: z.string().min(1, "Password is required"),
|
||||||
rememberMe: z.boolean().optional().default(false),
|
rememberMe: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -245,6 +245,57 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
|||||||
expect(response.json().code).toBe("VALIDATION_ERROR");
|
expect(response.json().code).toBe("VALIDATION_ERROR");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should register with trimmed username when input has whitespace", async () => {
|
||||||
|
const response = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/auth/register",
|
||||||
|
payload: {
|
||||||
|
username: " trimuser ",
|
||||||
|
password: "TestPassword123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(201);
|
||||||
|
expect(response.json().user.username).toBe("trimuser");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject whitespace-only username on registration", async () => {
|
||||||
|
const response = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/auth/register",
|
||||||
|
payload: {
|
||||||
|
username: " ",
|
||||||
|
password: "TestPassword123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.json().code).toBe("VALIDATION_ERROR");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject duplicate username even with surrounding whitespace", async () => {
|
||||||
|
await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/auth/register",
|
||||||
|
payload: {
|
||||||
|
username: "spacedupe",
|
||||||
|
password: "TestPassword123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/auth/register",
|
||||||
|
payload: {
|
||||||
|
username: " spacedupe ",
|
||||||
|
password: "AnotherPassword123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(409);
|
||||||
|
expect(response.json().code).toBe("USERNAME_EXISTS");
|
||||||
|
});
|
||||||
|
|
||||||
it("should reject invalid username characters", async () => {
|
it("should reject invalid username characters", async () => {
|
||||||
const response = await app.inject({
|
const response = await app.inject({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -341,6 +392,35 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
|||||||
expect(response.json().code).toBe("INVALID_CREDENTIALS");
|
expect(response.json().code).toBe("INVALID_CREDENTIALS");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should login successfully when username has leading/trailing whitespace", async () => {
|
||||||
|
const response = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/auth/login",
|
||||||
|
payload: {
|
||||||
|
username: " loginuser ",
|
||||||
|
password: "TestPassword123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.json().ok).toBe(true);
|
||||||
|
expect(response.json().user.username).toBe("loginuser");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject whitespace-only username on login", async () => {
|
||||||
|
const response = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: "/auth/login",
|
||||||
|
payload: {
|
||||||
|
username: " ",
|
||||||
|
password: "TestPassword123",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.json().code).toBe("VALIDATION_ERROR");
|
||||||
|
});
|
||||||
|
|
||||||
it("should support rememberMe option", async () => {
|
it("should support rememberMe option", async () => {
|
||||||
const response = await app.inject({
|
const response = await app.inject({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "medassist-ng-frontend",
|
"name": "medassist-ng-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.14.4",
|
"version": "1.15.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -275,6 +275,14 @@ export function MedDetailModal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="number-stepper refill-number-stepper">
|
<div className="number-stepper refill-number-stepper">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="stepper-btn decrement"
|
className="stepper-btn decrement"
|
||||||
@@ -284,14 +292,6 @@ export function MedDetailModal({
|
|||||||
>
|
>
|
||||||
<Minus size={16} aria-hidden="true" />
|
<Minus size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={min}
|
|
||||||
max={max}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
onBlur={onBlur}
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="stepper-btn increment"
|
className="stepper-btn increment"
|
||||||
@@ -321,16 +321,7 @@ export function MedDetailModal({
|
|||||||
const canIncrement = clamped < max;
|
const canIncrement = clamped < max;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="number-stepper">
|
<div className="number-stepper refill-number-stepper">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="stepper-btn decrement"
|
|
||||||
onClick={() => onChange(Math.max(min, clamped - 1))}
|
|
||||||
disabled={!canDecrement}
|
|
||||||
aria-label={decrementLabel}
|
|
||||||
>
|
|
||||||
<Minus size={16} aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={min}
|
min={min}
|
||||||
@@ -341,6 +332,15 @@ export function MedDetailModal({
|
|||||||
onChange(Number.isNaN(parsed) ? min : Math.min(max, Math.max(min, parsed)));
|
onChange(Number.isNaN(parsed) ? min : Math.min(max, Math.max(min, parsed)));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="stepper-btn decrement"
|
||||||
|
onClick={() => onChange(Math.max(min, clamped - 1))}
|
||||||
|
disabled={!canDecrement}
|
||||||
|
aria-label={decrementLabel}
|
||||||
|
>
|
||||||
|
<Minus size={16} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="stepper-btn increment"
|
className="stepper-btn increment"
|
||||||
|
|||||||
@@ -4549,7 +4549,7 @@ button.has-validation-error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.med-detail-body {
|
.med-detail-body {
|
||||||
padding: 1.5rem 2rem 0;
|
padding: 1.5rem 2rem 2rem;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,7 +446,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refill-number-stepper input {
|
.refill-number-stepper input {
|
||||||
order: initial;
|
order: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.75rem 0.5rem;
|
padding: 0.75rem 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -460,13 +460,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.refill-number-stepper .stepper-btn.decrement {
|
.refill-number-stepper .stepper-btn.decrement {
|
||||||
order: initial;
|
order: -1;
|
||||||
background: color-mix(in srgb, var(--danger) 22%, var(--bg-tertiary));
|
background: color-mix(in srgb, var(--danger) 22%, var(--bg-tertiary));
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.refill-number-stepper .stepper-btn.increment {
|
.refill-number-stepper .stepper-btn.increment {
|
||||||
order: initial;
|
order: 1;
|
||||||
border-right: none;
|
border-right: none;
|
||||||
border-left: 1px solid var(--border-primary);
|
border-left: 1px solid var(--border-primary);
|
||||||
background: color-mix(in srgb, var(--success) 22%, var(--bg-tertiary));
|
background: color-mix(in srgb, var(--success) 22%, var(--bg-tertiary));
|
||||||
|
|||||||
Reference in New Issue
Block a user