Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ jobs:
- name: Build frontend
run: npm run build
env:
VITE_APP_URL: http://localhost:2000
VITE_APP_front_end_url: http://localhost:5173
VITE_URL: http://localhost:2000
VITE_FRONT_END_URL: http://localhost:5173

backend:
name: Backend dependency check
Expand Down
15 changes: 11 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
X,
} from "lucide-react";
import { update_options } from "../../../store/optionsSlice";
import { API_BASE_URL } from "../../../config";
import { API_BASE_URL, FRONT_END_URL } from "../../../config";
import socket from "../../socket/Socket";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle } from "../../ui/dialog";
import { Button } from "../../ui/button";
Expand All @@ -35,7 +35,7 @@ function Navbar2ChatValid({ onNavigate }) {
const id = useSelector((state) => state.user_info.id);
const activeChannelId = useSelector((state) => state.currentPage.page_id);

const front_end_url = import.meta.env.VITE_FRONT_END_URL;
const front_end_url = FRONT_END_URL;

const [show, setShow] = useState(false);
const handleClose = () => {
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useDispatch, useSelector } from "react-redux";
import {
change_username,
change_tag,
change_bio,
option_profile_pic,
option_user_id,
set_notification_preferences,
Expand Down Expand Up @@ -143,11 +144,12 @@ function Dashboard() {
if (token) {
try {
const user_creds = jwt(token);
const { username, tag, profile_pic, id, notification_preferences } = user_creds;
const { username, tag, profile_pic, bio, id, notification_preferences } = user_creds;

dispatch(change_username(username));
dispatch(change_tag(tag));
dispatch(option_profile_pic(resolveProfilePic(profile_pic, username)));
dispatch(change_bio(bio || ""));
dispatch(option_user_id(id));

dispatch(
Expand Down
58 changes: 52 additions & 6 deletions frontend/src/components/register/Register.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,29 @@ function AlertBanner({ message, onClose }) {
);
}

function formatSignupError(data) {
if (data?.errors) {
const messages = Object.values(data.errors)
.flatMap((fieldErrors) =>
Object.values(fieldErrors).map((entry) => entry?.msg).filter(Boolean),
);
if (messages.length > 0) return messages.join(" ");
}
if (data?.message === "otp_storage_failed") {
return "Could not store verification code. Check server Redis configuration.";
}
return null;
}

function isStrongPassword(password) {
return (
password.length >= 7 &&
/[a-z]/.test(password) &&
/[A-Z]/.test(password) &&
/[0-9]/.test(password)
);
}

function Register() {
const Navigate = useNavigate();
const [days, setdays] = useState([]);
Expand Down Expand Up @@ -252,6 +275,19 @@ function Register() {
setdate_validation(true);
} else if (user_values.password !== user_values.confirm_password) {
setpassword_validation(true);
} else if (!isStrongPassword(user_values.password)) {
setalert_message(
"Password must be at least 7 characters and include uppercase, lowercase, and a number.",
);
setalert_box(true);
} else if (user_values.username.trim().length < 3) {
setalert_message("Username must be at least 3 characters.");
setalert_box(true);
} else if (!/^[a-zA-Z0-9._]+$/.test(user_values.username.trim())) {
setalert_message(
"Username can only contain letters, numbers, underscores, and dots.",
);
setalert_box(true);
} else {
setdate_validation(false);
setpassword_validation(false);
Expand Down Expand Up @@ -281,10 +317,20 @@ function Register() {
setalert_message("Please fill in all fields.");
setalert_box(true);
} else if (data.status === 400) {
setalert_message("Password must be at least 7 characters long.");
setalert_message(
formatSignupError(data) ||
"Password must be at least 7 characters and include uppercase, lowercase, and a number.",
);
setalert_box(true);
} else if (data.status === 500 && data.message === "otp_storage_failed") {
setalert_message(
"Could not store verification code. Check server Redis configuration.",
);
setalert_box(true);
} else {
setalert_message("Registration failed. Please try again.");
setalert_message(
formatSignupError(data) || "Registration failed. Please try again.",
);
setalert_box(true);
}
} catch {
Expand Down Expand Up @@ -431,7 +477,7 @@ function Register() {
onChange={handle_user_values}
required
disabled={submitting || verifying}
placeholder="At least 7 characters"
placeholder="e.g. MyPass123"
/>
<button
type="button"
Expand All @@ -448,7 +494,7 @@ function Register() {
</button>
</div>
<p className="mt-1 text-[11px]" style={{ color: "rgba(255,255,255,0.3)" }}>
Minimum 7 characters.
At least 7 characters with uppercase, lowercase, and a number.
</p>
</div>

Expand Down Expand Up @@ -532,8 +578,8 @@ function Register() {
required
>
<option value="" disabled>Month</option>
{months.map((m, idx) => (
<option key={`month-${idx + 1}`} value={idx + 1} style={{ background: "#1a1a2e", color: "#f0f0f5" }}>
{months.map((m) => (
<option key={`month-${m}`} value={m} style={{ background: "#1a1a2e", color: "#f0f0f5" }}>
{m}
</option>
))}
Expand Down
41 changes: 37 additions & 4 deletions frontend/src/components/settings/SettingsDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Save, Lock, Bell, User, Image, CheckCircle2, AlertCircle, Loader2, Penc

import { API_BASE_URL } from "../../config";
import { resolveProfilePic, handleImageError } from "../../shared/imageFallbacks";
import { change_tag, change_username, option_profile_pic, set_notification_preferences } from "../../store/userCredsSlice";
import { change_bio, change_tag, change_username, option_profile_pic, set_notification_preferences } from "../../store/userCredsSlice";
import { Dialog, DialogContent, DialogTitle, DialogDescription } from "../ui/dialog";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
Expand Down Expand Up @@ -36,6 +36,8 @@ async function uploadProfilePic(file) {
return data?.publicUrl || "";
}

const BIO_MAX_LENGTH = 190;

const DEFAULT_NOTIFICATION_PREFS = {
direct_messages: true,
friend_requests: true,
Expand Down Expand Up @@ -83,6 +85,7 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState("user");
const [displayName, setDisplayName] = useState(user.username || "");
const [bio, setBio] = useState(user.bio || "");
const [file, setFile] = useState(null);
const [previewUrl, setPreviewUrl] = useState("");
const [saving, setSaving] = useState(false);
Expand Down Expand Up @@ -111,8 +114,9 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
}, [previewUrl, user.profile_pic, user.username]);

useEffect(() => {
if (open && user.username) {
setDisplayName(user.username);
if (open) {
setDisplayName(user.username || "");
setBio(user.bio || "");
}
if (open) {
setActiveTab("user");
Expand All @@ -123,10 +127,11 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
setPasswordSuccess("");
setError("");
}
}, [open, user.username]);
}, [open, user.username, user.bio]);

const reset = () => {
setDisplayName(user.username || "");
setBio(user.bio || "");
setFile(null);
setPreviewUrl("");
setSaving(false);
Expand Down Expand Up @@ -187,6 +192,12 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
return;
}

const nextBio = String(bio || "").trim();
if (nextBio.length > BIO_MAX_LENGTH) {
setError(`Bio must be ${BIO_MAX_LENGTH} characters or fewer.`);
return;
}

setSaving(true);
setError("");
setSuccess("");
Expand All @@ -212,6 +223,7 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
body: JSON.stringify({
username: nextName,
profile_pic: finalProfilePic || "",
bio: nextBio,
}),
});

Expand Down Expand Up @@ -243,6 +255,7 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
dispatch(change_username(decoded.username));
dispatch(change_tag(decoded.tag));
dispatch(option_profile_pic(updatedProfilePic));
dispatch(change_bio(decoded.bio || nextBio));
if (decoded.notification_preferences) {
dispatch(set_notification_preferences(decoded.notification_preferences));
}
Expand Down Expand Up @@ -423,6 +436,26 @@ export default function SettingsDialog({ triggerClassName, icon: Icon }) {
/>
</div>
</div>

<div className="space-y-2">
<div className="flex items-center justify-between gap-3">
<label className="text-[10px] font-bold uppercase tracking-[0.2em] text-white/30">Bio</label>
<span className="text-[10px] text-white/30">
{bio.length}/{BIO_MAX_LENGTH}
</span>
</div>
<textarea
value={bio}
onChange={(e) => setBio(e.target.value.slice(0, BIO_MAX_LENGTH))}
placeholder="Tell others a little about yourself..."
rows={4}
maxLength={BIO_MAX_LENGTH}
className="w-full resize-none rounded-xl border border-white/5 bg-white/[0.03] px-4 py-3 text-sm text-white placeholder:text-white/10 outline-none transition-all duration-200 focus:border-violet-500/40 focus:bg-white/[0.05]"
/>
<p className="text-[11px] text-white/35">
This appears on your public profile.
</p>
</div>
</div>
)}

Expand Down
12 changes: 11 additions & 1 deletion frontend/src/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@ function normalizeOrigin(value) {
return String(value || "").trim().replace(/\/+$/, "");
}

const API_ORIGIN = normalizeOrigin(import.meta.env.VITE_URL);
const API_ORIGIN = normalizeOrigin(
import.meta.env.VITE_URL ||
import.meta.env.VITE_APP_URL ||
(import.meta.env.DEV ? "http://localhost:2000" : ""),
);

export const API_BASE_URL = API_ORIGIN ? `${API_ORIGIN}/api/v1` : "";
export const SOCKET_URL = API_ORIGIN;

export const FRONT_END_URL = normalizeOrigin(
import.meta.env.VITE_FRONT_END_URL ||
import.meta.env.VITE_APP_front_end_url ||
(import.meta.env.DEV ? "http://localhost:5173" : ""),
);
5 changes: 5 additions & 0 deletions frontend/src/store/userCredsSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const user_creds = createSlice({
username: "",
tag: "",
profile_pic: "",
bio: "",
id: 0,
notification_preferences: {
direct_messages: true,
Expand All @@ -24,6 +25,9 @@ export const user_creds = createSlice({
option_profile_pic: (state, action) => {
state.profile_pic = action.payload;
},
change_bio: (state, action) => {
state.bio = action.payload;
},
option_user_id: (state, action) => {
state.id = action.payload;
},
Expand All @@ -38,6 +42,7 @@ export const {
change_username,
change_tag,
option_profile_pic,
change_bio,
option_user_id,
set_notification_preferences,
} = user_creds.actions;
Expand Down
Loading