Skip to content

Commit 1a99c9c

Browse files
authored
Merge pull request #3 from Ovalvoi/main
Update Branch from Main
2 parents 91b6a41 + 58d1994 commit 1a99c9c

29 files changed

Lines changed: 159 additions & 3052 deletions

01-Database/GroundShareDB.sql

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ CREATE TABLE city (
2828
City_Name NVARCHAR(255) NOT NULL CHECK (LEN(TRIM(City_Name)) > 0),
2929
District NVARCHAR(255)
3030
);
31+
GO
3132

3233
CREATE TABLE event_type (
3334
EventType_ID INT IDENTITY(1,1) PRIMARY KEY,
3435
Name NVARCHAR(255) NOT NULL UNIQUE CHECK (LEN(TRIM(Name)) > 0)
3536
);
37+
GO
3638

3739
-- ============================================================
3840
-- 2. Streets (depends on city)
@@ -44,6 +46,7 @@ CREATE TABLE streets (
4446
Street_Name NVARCHAR(255) NOT NULL CHECK (LEN(TRIM(Street_Name)) > 0),
4547
UNIQUE (City_ID, Street_Name)
4648
);
49+
GO
4750

4851
-- ============================================================
4952
-- 3. Location (depends on city, streets)
@@ -59,8 +62,10 @@ CREATE TABLE location (
5962
Latitude FLOAT,
6063
Longitude FLOAT
6164
);
65+
GO
6266

6367
CREATE INDEX IX_location_geo ON location(Latitude, Longitude) WHERE Latitude IS NOT NULL;
68+
GO
6469

6570
-- ============================================================
6671
-- 4. Users (depends on location)
@@ -82,6 +87,7 @@ CREATE TABLE [user] (
8287
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
8388
Is_Active BIT NOT NULL DEFAULT 1
8489
);
90+
GO
8591

8692
-- ============================================================
8793
-- 5. Refresh tokens (for JWT rotation)
@@ -95,8 +101,10 @@ CREATE TABLE refresh_token (
95101
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
96102
Revoked_At DATETIME2
97103
);
104+
GO
98105

99106
CREATE INDEX IX_refresh_token_user ON refresh_token(User_ID);
107+
GO
100108

101109
-- ============================================================
102110
-- 6. Events / Reports (depends on location, event_type, user)
@@ -116,9 +124,12 @@ CREATE TABLE event (
116124
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
117125
CHECK (End_Date IS NULL OR End_Date >= Start_Date)
118126
);
127+
GO
119128

120129
CREATE INDEX IX_event_location ON event(Location_ID);
130+
GO
121131
CREATE INDEX IX_event_user ON event(User_ID);
132+
GO
122133

123134
-- ============================================================
124135
-- 7. Event votes (relevancy — like Waze)
@@ -131,6 +142,7 @@ CREATE TABLE event_vote (
131142
Voted_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
132143
PRIMARY KEY (User_ID, Event_ID)
133144
);
145+
GO
134146

135147
-- ============================================================
136148
-- 8. Comments on events
@@ -144,8 +156,10 @@ CREATE TABLE comment (
144156
Picture NVARCHAR(MAX),
145157
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
146158
);
159+
GO
147160

148161
CREATE INDEX IX_comment_event ON comment(Event_ID);
162+
GO
149163

150164
-- ============================================================
151165
-- 9. Reviews (on locations, with good/bad sentiment)
@@ -161,9 +175,12 @@ CREATE TABLE user_review (
161175
Picture NVARCHAR(MAX),
162176
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
163177
);
178+
GO
164179

165180
CREATE INDEX IX_review_location ON user_review(Location_ID);
181+
GO
166182
CREATE UNIQUE INDEX UQ_review_user_location ON user_review(User_ID, Location_ID);
183+
GO
167184

168185
-- ============================================================
169186
-- 10. Favorites
@@ -175,6 +192,7 @@ CREATE TABLE add_to_favorites (
175192
Added_Date DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
176193
PRIMARY KEY (User_ID, Location_ID)
177194
);
195+
GO
178196

179197
-- ============================================================
180198
-- 11. Plan status (building permits)
@@ -191,6 +209,7 @@ CREATE TABLE PlanStatus (
191209
Current_Stage NVARCHAR(255) DEFAULT N'Initiated',
192210
CHECK (End_Date IS NULL OR End_Date >= Start_Date)
193211
);
212+
GO
194213

195214
-- ============================================================
196215
-- 12. Comparison history
@@ -201,6 +220,7 @@ CREATE TABLE comparison (
201220
User_ID INT NOT NULL REFERENCES [user](User_ID),
202221
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
203222
);
223+
GO
204224

205225
CREATE TABLE comparison_address (
206226
Comparison_ID INT NOT NULL REFERENCES comparison(Comparison_ID),
@@ -210,6 +230,7 @@ CREATE TABLE comparison_address (
210230
Address NVARCHAR(500),
211231
PRIMARY KEY (Comparison_ID, Location_ID)
212232
);
233+
GO
213234

214235
-- ============================================================
215236
-- 13. Survey / onboarding answers
@@ -232,6 +253,7 @@ CREATE TABLE survey (
232253
Is_GovRepresentative BIT NOT NULL DEFAULT 0, -- אני נציג/ת רשות / עובד/ת עירייה
233254
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
234255
);
256+
GO
235257

236258
-- ============================================================
237259
-- 14. User activity log (for gamification feed)
@@ -245,8 +267,10 @@ CREATE TABLE user_activity (
245267
XP_Earned INT NOT NULL DEFAULT 0,
246268
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME()
247269
);
270+
GO
248271

249272
CREATE INDEX IX_activity_user ON user_activity(User_ID, Created_At DESC);
273+
GO
250274

251275
-- ============================================================
252276
-- 15. Notification subscriptions (bell toggle per location)
@@ -258,6 +282,7 @@ CREATE TABLE notification_subscription (
258282
Created_At DATETIME2 NOT NULL DEFAULT SYSUTCDATETIME(),
259283
PRIMARY KEY (User_ID, Location_ID)
260284
);
285+
GO
261286

262287
-- ============================================================
263288
-- Seed event types
@@ -485,8 +510,10 @@ AS
485510
BEGIN
486511
SET NOCOUNT ON;
487512
SELECT e.Event_ID, e.Description, e.EventStatus, e.Start_Date, e.End_Date, e.Picture, e.Created_At,
513+
et.Name AS EventType_Name,
488514
et.Name AS EventType,
489515
u.Full_Name AS Author,
516+
ISNULL((SELECT SUM(Vote) FROM event_vote WHERE Event_ID = e.Event_ID), 0) AS Vote_Count,
490517
ISNULL((SELECT SUM(Vote) FROM event_vote WHERE Event_ID = e.Event_ID), 0) AS VoteCount
491518
FROM event e
492519
INNER JOIN event_type et ON e.EventType_ID = et.EventType_ID
@@ -572,8 +599,10 @@ BEGIN
572599
END
573600
END
574601

575-
-- Return new total
576-
SELECT ISNULL(SUM(Vote), 0) AS VoteCount FROM event_vote WHERE Event_ID = @Event_ID;
602+
-- Return new total (both aliases during additive migration)
603+
SELECT ISNULL(SUM(Vote), 0) AS Vote_Count,
604+
ISNULL(SUM(Vote), 0) AS VoteCount
605+
FROM event_vote WHERE Event_ID = @Event_ID;
577606
END
578607
GO
579608

02-Server/Controllers/AuthController.cs

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
using System.Text.RegularExpressions;
2222
using Google.Apis.Auth;
2323
using Microsoft.Extensions.Options;
24-
using WebApplication1.Options;
24+
using GroundShareAPI.Options;
2525

2626
namespace GroundShareAPI.Controllers
2727
{
@@ -75,39 +75,42 @@ public async Task<IActionResult> Register([FromBody] RegisterRequest request)
7575
var streetName = Sanitize(request.Street_Name);
7676
var houseNumber = Sanitize(request.House_Number);
7777

78-
// --- Email validation ---
78+
// Validation errors flow through ExceptionHandlingMiddleware so the
79+
// client sees one ApiResponse<object> shape for every failure.
80+
var fieldErrors = new Dictionary<string, string>();
81+
7982
if (string.IsNullOrEmpty(email))
80-
return BadRequest(new { field = "email", message = "כתובת מייל היא שדה חובה" });
81-
if (email.Length > 254)
82-
return BadRequest(new { field = "email", message = "כתובת מייל ארוכה מדי" });
83-
if (!EmailRegex.IsMatch(email))
84-
return BadRequest(new { field = "email", message = "כתובת מייל לא תקינה" });
83+
fieldErrors["email"] = "כתובת מייל היא שדה חובה";
84+
else if (email.Length > 254)
85+
fieldErrors["email"] = "כתובת מייל ארוכה מדי";
86+
else if (!EmailRegex.IsMatch(email))
87+
fieldErrors["email"] = "כתובת מייל לא תקינה";
8588

86-
// --- Password validation ---
8789
if (string.IsNullOrEmpty(password))
88-
return BadRequest(new { field = "password", message = "סיסמה היא שדה חובה" });
89-
if (password.Length < 8)
90-
return BadRequest(new { field = "password", message = "הסיסמה חייבת להכיל לפחות 8 תווים" });
91-
if (password.Length > 128)
92-
return BadRequest(new { field = "password", message = "הסיסמה ארוכה מדי (מקסימום 128 תווים)" });
93-
if (!password.Any(char.IsUpper))
94-
return BadRequest(new { field = "password", message = "הסיסמה חייבת לכלול לפחות אות גדולה באנגלית" });
95-
if (!password.Any(char.IsLower))
96-
return BadRequest(new { field = "password", message = "הסיסמה חייבת לכלול לפחות אות קטנה באנגלית" });
97-
if (!password.Any(char.IsDigit))
98-
return BadRequest(new { field = "password", message = "הסיסמה חייבת לכלול לפחות ספרה אחת" });
99-
100-
// --- Full name validation ---
90+
fieldErrors["password"] = "סיסמה היא שדה חובה";
91+
else if (password.Length < 8)
92+
fieldErrors["password"] = "הסיסמה חייבת להכיל לפחות 8 תווים";
93+
else if (password.Length > 128)
94+
fieldErrors["password"] = "הסיסמה ארוכה מדי (מקסימום 128 תווים)";
95+
else if (!password.Any(char.IsUpper))
96+
fieldErrors["password"] = "הסיסמה חייבת לכלול לפחות אות גדולה באנגלית";
97+
else if (!password.Any(char.IsLower))
98+
fieldErrors["password"] = "הסיסמה חייבת לכלול לפחות אות קטנה באנגלית";
99+
else if (!password.Any(char.IsDigit))
100+
fieldErrors["password"] = "הסיסמה חייבת לכלול לפחות ספרה אחת";
101+
101102
if (string.IsNullOrEmpty(fullName))
102-
return BadRequest(new { field = "fullName", message = "שם מלא הוא שדה חובה" });
103-
if (fullName.Length < 2)
104-
return BadRequest(new { field = "fullName", message = "שם חייב להכיל לפחות 2 תווים" });
105-
if (fullName.Length > 50)
106-
return BadRequest(new { field = "fullName", message = "שם ארוך מדי (מקסימום 50 תווים)" });
103+
fieldErrors["fullName"] = "שם מלא הוא שדה חובה";
104+
else if (fullName.Length < 2)
105+
fieldErrors["fullName"] = "שם חייב להכיל לפחות 2 תווים";
106+
else if (fullName.Length > 50)
107+
fieldErrors["fullName"] = "שם ארוך מדי (מקסימום 50 תווים)";
107108

108-
// --- Phone validation (optional) ---
109109
if (!string.IsNullOrEmpty(phone) && !PhoneRegex.IsMatch(phone))
110-
return BadRequest(new { field = "phone", message = "מספר טלפון לא תקין" });
110+
fieldErrors["phone"] = "מספר טלפון לא תקין";
111+
112+
if (fieldErrors.Count > 0)
113+
throw new Exceptions.ValidationException("נתוני הרשמה אינם תקינים", fieldErrors);
111114

112115
try
113116
{
@@ -121,7 +124,7 @@ public async Task<IActionResult> Register([FromBody] RegisterRequest request)
121124
string.IsNullOrEmpty(houseNumber) ? null : houseNumber);
122125

123126
if (userRow == null)
124-
return StatusCode(500, new { message = "שגיאה בהרשמה" });
127+
throw new InvalidOperationException("User row was null after successful register.");
125128

126129
int userId = Convert.ToInt32(userRow["User_ID"]);
127130
string accessToken = GenerateAccessToken(userId, email);
@@ -151,7 +154,7 @@ public async Task<IActionResult> Register([FromBody] RegisterRequest request)
151154
public async Task<IActionResult> Login([FromBody] LoginRequest request)
152155
{
153156
if (string.IsNullOrWhiteSpace(request.Email) || string.IsNullOrWhiteSpace(request.Password))
154-
return BadRequest(new { message = "Email and password are required." });
157+
throw new Exceptions.ValidationException("Email and password are required.");
155158

156159
var userRow = await _usersDal.GetUserByEmailRawAsync(request.Email);
157160

@@ -174,7 +177,7 @@ public async Task<IActionResult> Login([FromBody] LoginRequest request)
174177
{
175178
await _audit.LogAsync(null, "LOGIN_FAILURE", ClientIp, ClientUserAgent,
176179
JsonSerializer.Serialize(new { email = request.Email }));
177-
return Unauthorized(new { message = "אימות נכשל" });
180+
throw new UnauthorizedException("אימות נכשל");
178181
}
179182

180183
int userId = Convert.ToInt32(userRow["User_ID"]);
@@ -202,7 +205,7 @@ await _audit.LogAsync(null, "LOGIN_FAILURE", ClientIp, ClientUserAgent,
202205
public async Task<IActionResult> GoogleSignIn([FromBody] GoogleSignInRequest request)
203206
{
204207
if (string.IsNullOrWhiteSpace(request?.IdToken))
205-
return BadRequest(new { message = "Missing Google credential." });
208+
throw new Exceptions.ValidationException("Missing Google credential.");
206209

207210
// -----------------------------------------------------------------
208211
// Verify the ID token using Google's official library.
@@ -227,65 +230,58 @@ public async Task<IActionResult> GoogleSignIn([FromBody] GoogleSignInRequest req
227230
});
228231

229232
if (!payload.EmailVerified)
230-
return Unauthorized(new { message = "Google email is not verified." });
233+
throw new UnauthorizedException("Google email is not verified.");
231234

232235
email = payload.Email ?? "";
233236
fullName = payload.Name ?? payload.GivenName ?? email.Split('@')[0];
234237

235238
if (string.IsNullOrWhiteSpace(email))
236-
return Unauthorized(new { message = "Google token missing email." });
239+
throw new UnauthorizedException("Google token missing email.");
237240
}
238241
catch (InvalidJwtException)
239242
{
240-
return Unauthorized(new { message = "Invalid Google token." });
243+
throw new UnauthorizedException("Invalid Google token.");
241244
}
242245

243246
email = Sanitize(email).ToLowerInvariant();
244247
fullName = Sanitize(fullName);
245248
if (fullName.Length > 50) fullName = fullName.Substring(0, 50);
246249
if (string.IsNullOrWhiteSpace(fullName)) fullName = email.Split('@')[0];
247250

248-
try
249-
{
250-
var userRow = await _socialAuthDal.UpsertGoogleUserAsync(email, fullName);
251-
if (userRow == null)
252-
return StatusCode(500, new { message = "Failed to create or load user." });
251+
var userRow = await _socialAuthDal.UpsertGoogleUserAsync(email, fullName);
252+
if (userRow == null)
253+
throw new InvalidOperationException("Failed to create or load Google user.");
253254

254-
int userId = Convert.ToInt32(userRow["User_ID"]);
255-
string accessToken = GenerateAccessToken(userId, email);
256-
string refreshToken = GenerateRefreshToken();
255+
int userId = Convert.ToInt32(userRow["User_ID"]);
256+
string accessToken = GenerateAccessToken(userId, email);
257+
string refreshToken = GenerateRefreshToken();
257258

258-
await _refreshTokenDal.SaveTokenAsync(userId, refreshToken, DateTime.UtcNow.AddDays(7));
259-
await _audit.LogAsync(userId, "GOOGLE_SIGNIN", ClientIp, ClientUserAgent);
259+
await _refreshTokenDal.SaveTokenAsync(userId, refreshToken, DateTime.UtcNow.AddDays(7));
260+
await _audit.LogAsync(userId, "GOOGLE_SIGNIN", ClientIp, ClientUserAgent);
260261

261-
return Ok(new
262-
{
263-
accessToken,
264-
refreshToken,
265-
user = userRow
266-
});
267-
}
268-
catch (Exception)
262+
return Ok(new
269263
{
270-
return StatusCode(500, new { message = "An error occurred during Google sign-in." });
271-
}
264+
accessToken,
265+
refreshToken,
266+
user = userRow
267+
});
272268
}
273269

274270
[HttpPost("refresh")]
275271
[EnableRateLimiting("auth-refresh")]
276272
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
277273
{
278274
if (string.IsNullOrWhiteSpace(request.RefreshToken))
279-
return BadRequest(new { message = "Refresh token is required." });
275+
throw new Exceptions.ValidationException("Refresh token is required.");
280276

281277
var tokenRow = await _refreshTokenDal.GetTokenAsync(request.RefreshToken);
282278

283279
if (tokenRow == null)
284-
return Unauthorized(new { message = "Invalid refresh token." });
280+
throw new UnauthorizedException("Invalid refresh token.");
285281

286282
var expiresAt = Convert.ToDateTime(tokenRow["Expires_At"]);
287283
if (expiresAt < DateTime.UtcNow)
288-
return Unauthorized(new { message = "Refresh token expired." });
284+
throw new UnauthorizedException("Refresh token expired.");
289285

290286
// Revoke old token
291287
await _refreshTokenDal.RevokeTokenAsync(request.RefreshToken);
@@ -295,7 +291,7 @@ public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
295291
// Get user data
296292
var userRow = await _usersDal.GetUserByIdAsync(userId);
297293
if (userRow == null)
298-
return Unauthorized(new { message = "User not found." });
294+
throw new UnauthorizedException("User not found.");
299295

300296
string email = userRow["Email"]?.ToString() ?? "";
301297
string newAccessToken = GenerateAccessToken(userId, email);

02-Server/Controllers/EventsController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public async Task<IActionResult> Vote(int eventId, [FromBody] VoteRequest reques
5959
{
6060
int userId = GetUserId();
6161
int newCount = await _eventService.VoteOnEventAsync(userId, eventId, request.Vote);
62-
return Ok(new { VoteCount = newCount });
62+
return Ok(new { Vote_Count = newCount, VoteCount = newCount });
6363
}
6464

6565
[HttpGet("{eventId}/comments")]

0 commit comments

Comments
 (0)