Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DonationService.Data;
Expand All @@ -10,6 +12,7 @@ namespace DonationService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
[Authorize]
public class DonationController : ControllerBase
{
private readonly DonationDbContext _context;
Expand Down
3 changes: 3 additions & 0 deletions Glense.Server/DonationService/Controllers/WalletController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DonationService.Data;
Expand All @@ -9,6 +11,7 @@ namespace DonationService.Controllers;
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
[Authorize]
public class WalletController : ControllerBase
{
private readonly DonationDbContext _context;
Expand Down
1 change: 1 addition & 0 deletions Glense.Server/DonationService/DonationService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.12" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.0" />
</ItemGroup>
Expand Down
44 changes: 39 additions & 5 deletions Glense.Server/DonationService/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using DonationService.Data;
using DonationService.Services;

Expand Down Expand Up @@ -41,6 +44,33 @@

builder.Services.AddScoped<IAccountServiceClient, AccountServiceClient>();

// JWT Authentication
var jwtIssuer = builder.Configuration["JwtSettings:Issuer"] ?? "GlenseAccountService";
var jwtAudience = builder.Configuration["JwtSettings:Audience"] ?? "GlenseApp";
var jwtSecret = builder.Configuration["JwtSettings:SecretKey"]
?? Environment.GetEnvironmentVariable("JWT_SECRET_KEY")
?? throw new InvalidOperationException("JWT secret key is not configured");

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtIssuer,
ValidateAudience = true,
ValidAudience = jwtAudience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30)
};
});

builder.Services.AddAuthorization();

// Health check endpoint for container orchestration
builder.Services.AddHealthChecks();

Expand All @@ -62,13 +92,17 @@
app.UseCors();

// Swagger UI available at root path for easy API exploration
app.UseSwagger();
app.UseSwaggerUI(c =>
if (app.Environment.IsDevelopment())
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Donation Microservice API v1");
c.RoutePrefix = string.Empty;
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Donation Microservice API v1");
c.RoutePrefix = string.Empty;
});
}

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
Expand Down
24 changes: 23 additions & 1 deletion dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,34 @@ do_prune() {
echo "Done"
}

do_nuke() {
echo "Stopping ALL containers..."
$RUNTIME stop -a 2>/dev/null
echo "Removing ALL containers..."
$RUNTIME rm -a -f 2>/dev/null
echo "Removing ALL volumes..."
$RUNTIME volume prune -f 2>/dev/null
echo "Removing ALL images..."
$RUNTIME rmi -a -f 2>/dev/null
echo ""
echo "Everything wiped. Run './dev.sh up' to start fresh."
}

do_reset() {
echo "Full reset: nuke + rebuild + seed"
do_nuke
echo ""
do_up
}

case "${1:-up}" in
up) do_up ;;
down) do_down ;;
restart) do_down; echo ""; do_up ;;
logs) do_logs "$2" ;;
seed) do_seed ;;
prune) do_prune ;;
*) echo "Usage: ./dev.sh [up|down|restart|logs|seed|prune]" ;;
nuke) do_nuke ;;
reset) do_reset ;;
*) echo "Usage: ./dev.sh [up|down|restart|logs|seed|nuke|reset|prune]" ;;
esac
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ services:
- PORT=5100
- DONATION_DB_CONNECTION_STRING=Host=postgres_donation;Port=5432;Database=glense_donation;Username=glense;Password=glense123
- ACCOUNT_SERVICE_URL=http://account_service:5000
- JwtSettings__Issuer=GlenseAccountService
- JwtSettings__Audience=GlenseApp
- JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm
ports:
- "5100:5100"
depends_on:
Expand Down Expand Up @@ -133,6 +136,9 @@ services:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__VideoCatalogue=Host=postgres_video;Port=5432;Database=glense_video;Username=glense;Password=glense123
- ACCOUNT_SERVICE_URL=http://account_service:5000
- JwtSettings__Issuer=GlenseAccountService
- JwtSettings__Audience=GlenseApp
- JwtSettings__SecretKey=YourSuperSecretKeyThatIsAtLeast32CharactersLongForHS256Algorithm
ports:
- "5002:5002"
depends_on:
Expand Down
45 changes: 32 additions & 13 deletions glense.client/src/components/Chat/Chat.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,48 @@ import { useState, useEffect } from "react";
import ChatWindow from "./ChatWindow";
import ChatSidebar from "./ChatSidebar";
import chatService from "../../utils/chatService";
import { useAuth } from "../../context/AuthContext";
import "../../css/Chat/Chat.css";

function getOtherName(topic, myName) {
if (!topic) return 'Chat';
const parts = topic.split(':');
if (parts.length === 2) {
return parts[0] === myName ? parts[1] : parts[0];
}
return topic;
}

function Chat() {
const [chats, setChats] = useState([]);
const [selectedChat, setSelectedChat] = useState(null);
const [displayName, setDisplayName] = useState(() => {
try { return localStorage.getItem('chat.displayName') || 'Alice'; } catch { return 'Alice'; }
});
const { user } = useAuth();
const displayName = user?.username || 'Anonymous';
const localSender = 'user';

const normalizeItems = (res) => {
if (!res) return [];
if (Array.isArray(res)) return res;
return res.items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.Items || res.items || [];
return res.items || res.Items || [];
};

const enrichChat = (chat) => {
const topic = chat.topic || chat.Topic || '';
return { ...chat, displayName: getOtherName(topic, displayName) };
};

const loadChats = async () => {
try {
const res = await chatService.getChats();
const items = normalizeItems(res);
const items = normalizeItems(res)
.filter(c => {
const topic = c.topic || c.Topic || '';
const parts = topic.split(':');
return parts.includes(displayName);
})
.map(enrichChat);
setChats(items);
if (!selectedChat && items.length) {
// auto-select first
const first = items[0];
setSelectedChat(first);
await loadMessages(first);
Expand All @@ -51,7 +70,7 @@ function Chat() {
isMe: senderName === displayName
};
});
setSelectedChat(prev => ({ ...chat, messages: msgs }));
setSelectedChat(prev => ({ ...enrichChat(chat), messages: msgs }));
} catch (err) {
console.error("getMessages", err);
}
Expand All @@ -60,19 +79,19 @@ function Chat() {
useEffect(() => { loadChats(); }, []);

const handleSelect = async (chat) => {
setSelectedChat(chat);
setSelectedChat(enrichChat(chat));
await loadMessages(chat);
};

const handleCreate = async (topic) => {
const handleCreate = async (otherUsername) => {
try {
const topic = `${displayName}:${otherUsername}`;
const created = await chatService.createChat({ topic });
// reload chats and select created
await loadChats();
const id = created?.id || created?.Id || created?.chatId;
if (id) {
const c = (await chatService.getChat(id));
handleSelect(c || { id });
const c = await chatService.getChat(id);
handleSelect(c || { id, topic });
}
} catch (err) {
console.error('createChat', err);
Expand Down Expand Up @@ -103,7 +122,7 @@ function Chat() {
};

return (
<div className="chat-container">
<div className="chat-container">
<ChatSidebar chats={chats} onSelectChat={handleSelect} onCreate={handleCreate} />
<ChatWindow chat={selectedChat || { profileImage: '', name: '', messages: [] }} onSend={handleSend} />
</div>
Expand Down
75 changes: 61 additions & 14 deletions glense.client/src/components/Chat/ChatSidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { useState } from "react";
import { Box, List, ListItem, ListItemAvatar, Avatar, ListItemText, TextField, Button, ListItemButton } from "@mui/material";
import "../../css/Chat/ChatSideBar.css";
import { useState, useEffect, useRef } from "react";
import { Box, List, ListItem, ListItemAvatar, Avatar, ListItemText, TextField, ListItemButton, Typography, Stack } from "@mui/material";
import { profileService } from "../../services/profileService";
import { useAuth } from "../../context/AuthContext";
import "../../css/Chat/ChatSidebar.css";

const colors = ['#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50', '#ff9800', '#ff5722'];
function stringToColor(str) {
Expand All @@ -10,34 +12,79 @@ function stringToColor(str) {
}

const ChatSidebar = ({ chats, onSelectChat, onCreate }) => {
const [topic, setTopic] = useState("");
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [searching, setSearching] = useState(false);
const { user } = useAuth();
const timerRef = useRef(null);

const handleCreate = async () => {
if (!topic) return;
useEffect(() => {
if (!query.trim()) { setResults([]); return; }
clearTimeout(timerRef.current);
timerRef.current = setTimeout(async () => {
setSearching(true);
try {
const users = await profileService.searchUsers(query, 10);
setResults((users || []).filter(u => u.id !== user?.id));
} catch { setResults([]); }
setSearching(false);
}, 300);
return () => clearTimeout(timerRef.current);
}, [query, user?.id]);

const handleSelectUser = async (selectedUser) => {
try {
await onCreate?.(topic);
setTopic("");
await onCreate?.(selectedUser.username);
setQuery("");
setResults([]);
} catch (err) {
console.error("create chat", err);
}
};

return (
<div className="chat-sidebar">
<Box sx={{ display: 'flex', gap: 1, p: 1 }}>
<TextField size="small" placeholder="New chat" value={topic} onChange={e => setTopic(e.target.value)} fullWidth />
<Button variant="contained" size="small" onClick={handleCreate}>Create</Button>
<Box sx={{ p: 1, position: 'relative' }}>
<TextField
size="small"
placeholder="Search users..."
value={query}
onChange={e => setQuery(e.target.value)}
fullWidth
className="chat-search-input"
/>
{results.length > 0 && (
<Box className="chat-search-results">
{results.map(u => (
<Stack
key={u.id}
direction="row"
className="chat-search-item"
onClick={() => handleSelectUser(u)}
>
<Avatar sx={{ bgcolor: stringToColor(u.username), width: 32, height: 32, fontSize: 14 }}>
{u.username?.charAt(0).toUpperCase()}
</Avatar>
<Typography className="chat-search-name">{u.username}</Typography>
</Stack>
))}
</Box>
)}
{searching && <Typography className="chat-search-hint">Searching...</Typography>}
{query && !searching && results.length === 0 && (
<Typography className="chat-search-hint">No users found</Typography>
)}
</Box>
<List>
{chats.map((chat, index) => (
<ListItem key={index} className="chat-sidebar-item" disablePadding>
<ListItemButton onClick={() => onSelectChat(chat)}>
<ListItemAvatar>
<Avatar sx={{ bgcolor: stringToColor(chat.topic || chat.Topic || chat.name || ''), fontSize: 16 }}>
{(chat.topic || chat.Topic || chat.name || '?').charAt(0).toUpperCase()}
<Avatar sx={{ bgcolor: stringToColor(chat.displayName || chat.topic || ''), fontSize: 16 }}>
{(chat.displayName || chat.topic || '?').charAt(0).toUpperCase()}
</Avatar>
</ListItemAvatar>
<ListItemText primary={chat.name || chat.Topic || chat.topic || chat.title || 'Untitled'} />
<ListItemText primary={chat.displayName || chat.topic || 'Untitled'} />
</ListItemButton>
</ListItem>
))}
Expand Down
6 changes: 3 additions & 3 deletions glense.client/src/components/Chat/ChatWindow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ const ChatWindow = ({ chat, onSend }) => {
<div className="chat-window">
{/* Chat Header */}
<div className="chat-window-header">
<Avatar sx={{ bgcolor: stringToColor(chat.topic || chat.Topic || chat.name || ''), width: 36, height: 36, fontSize: 16 }}>
{(chat.topic || chat.Topic || chat.name || '?').charAt(0).toUpperCase()}
<Avatar sx={{ bgcolor: stringToColor(chat.displayName || chat.topic || ''), width: 36, height: 36, fontSize: 16 }}>
{(chat.displayName || chat.topic || '?').charAt(0).toUpperCase()}
</Avatar>
<span className="chat-window-header-name">{chat.name || chat.Topic || chat.topic || chat.title || 'Chat'}</span>
<span className="chat-window-header-name">{chat.displayName || chat.topic || 'Chat'}</span>
</div>

{/* Chat Messages */}
Expand Down
Loading
Loading