diff --git a/OnlyPrompt.Backend/ApiModels/Auth/Requests.cs b/OnlyPrompt.Backend/ApiModels/Auth/Requests.cs index fd26cb5..929cfef 100644 --- a/OnlyPrompt.Backend/ApiModels/Auth/Requests.cs +++ b/OnlyPrompt.Backend/ApiModels/Auth/Requests.cs @@ -4,5 +4,5 @@ using System.ComponentModel.DataAnnotations; namespace OnlyPrompt.Backend.ApiModels.Auth { public record ApiLoginRequest(string UserNameOrEmail, string Password); - public record ApiRegisterRequest([MaxLength(100)] string DisplayName, [MaxLength(100)][NoWhitespace] string UserName, string Email, string Password); + public record ApiRegisterRequest([MaxLength(100)] string DisplayName, [MaxLength(100)][NoWhitespace] string? UserName, string Email, string Password); } diff --git a/OnlyPrompt.Backend/ApiModels/Validators/NoWhitespaceAttribute.cs b/OnlyPrompt.Backend/ApiModels/Validators/NoWhitespaceAttribute.cs index dd4b7eb..a7f0367 100644 --- a/OnlyPrompt.Backend/ApiModels/Validators/NoWhitespaceAttribute.cs +++ b/OnlyPrompt.Backend/ApiModels/Validators/NoWhitespaceAttribute.cs @@ -1,10 +1,22 @@ using System.ComponentModel.DataAnnotations; -using System.Reflection.Metadata.Ecma335; namespace OnlyPrompt.Backend.ApiModels.Validators { public class NoWhitespaceAttribute : ValidationAttribute { + public override bool IsValid(object? value) + { + if (value is string strValue) + { + if (strValue.Any(c => char.IsWhiteSpace(c))) + return false; + + return true; + } + + return true; // If it's not a string, we consider it valid. Use [NoWhitespace] only on string properties. + } + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) { if(value is string strValue) @@ -15,7 +27,7 @@ namespace OnlyPrompt.Backend.ApiModels.Validators return ValidationResult.Success; } - return base.IsValid(value, validationContext); + return ValidationResult.Success; // If it's not a string, we consider it valid. Use [NoWhitespace] only on string properties. } } diff --git a/OnlyPrompt.Backend/Controllers/AuthController.cs b/OnlyPrompt.Backend/Controllers/AuthController.cs index 292354f..daa2605 100644 --- a/OnlyPrompt.Backend/Controllers/AuthController.cs +++ b/OnlyPrompt.Backend/Controllers/AuthController.cs @@ -54,7 +54,7 @@ namespace OnlyPrompt.Backend.Controllers [AllowAnonymous] [HttpPost("register")] - public async Task>> RegisterAsync([FromBody] ApiRegisterRequest request) + public async Task>> RegisterAsync([FromBody] ApiRegisterRequest request, [FromQuery] string redirect = null) { var existingUser = await FindUserAsync(request.UserName, request.Email); if (existingUser is not null) @@ -84,7 +84,7 @@ namespace OnlyPrompt.Backend.Controllers }, Roles = [ModelConstants.UserRole], PasswordHash = null, - UserName = request.UserName, + UserName = request.UserName ?? request.Email, Email = request.Email, IsLockoutEnabled = false, }; @@ -92,6 +92,9 @@ namespace OnlyPrompt.Backend.Controllers newUser.PasswordHash = _passwordHasher.HashPassword(newUser, request.Password); _db.Users.Add(newUser); await _db.SaveChangesAsync(); + if(string.IsNullOrEmpty(redirect) == false) + return TypedResults.Redirect(redirect, false); + return TypedResults.Ok(_mapper.Map(newUser)); } diff --git a/OnlyPrompt.Frontend/js/shared.js b/OnlyPrompt.Frontend/js/shared.js index 7f3e57e..13d0c7b 100644 --- a/OnlyPrompt.Frontend/js/shared.js +++ b/OnlyPrompt.Frontend/js/shared.js @@ -32,6 +32,11 @@ export async function sendFormAsync(form, url, method) { method = method || form.method || 'post'; const data = formToObject(form); const response = await sendJsonAsync(url, data, method); + if (response.ok && response.redirected) { + window.location.href = response.url; + return null; + } + const responseText = await response.text(); if (response.ok == false && handleValidationError(response, responseText, form)) { return null; @@ -41,11 +46,6 @@ export async function sendFormAsync(form, url, method) { handleGenericFormError(response, responseText, form); return null; } else { - if(response.redirected){ - window.location.href = response.url; - return null; - } - return responseText.length == 0 ? null : JSON.parse(responseText); } } @@ -115,13 +115,25 @@ const unknownInputErrorTemplate = new Template(` `); +function toCamelCase(str) { + str = str.replace(/([-_][a-z])/gi, (match) => { + return match.toUpperCase() + .replace('-', '') + .replace('_', ''); + }); + + str = str[0].toLowerCase() + str.substring(1); + return str; +} + + function handleValidationError(response, responseText, form) { if (response.status !== 400) return false; const responseObject = JSON.parse(responseText); const unknownInputErrors = {}; if (responseObject.type === 'https://tools.ietf.org/html/rfc9110#section-15.5.1' && responseObject.errors) { for (const [field, messages] of Object.entries(responseObject.errors)) { - const input = form.querySelector(`[name="${field}"]`); + const input = form.querySelector(`[name="${toCamelCase(field)}"]`); if (input) { const parent = input.parentElement; const errorHtml = validationErrorTemplate.render(messages); diff --git a/OnlyPrompt.Frontend/js/signup.js b/OnlyPrompt.Frontend/js/signup.js new file mode 100644 index 0000000..2ba88be --- /dev/null +++ b/OnlyPrompt.Frontend/js/signup.js @@ -0,0 +1,12 @@ +import { sendFormAsync } from "./shared.js"; + +async function signupAsync(params) { + const form = document.getElementById('signupForm'); + await sendFormAsync(form); +} + +const signupForm = document.getElementById('signupForm'); +signupForm.addEventListener('submit', async (event) => { + event.preventDefault(); // Prevent the default form submission + await signupAsync(); +}); \ No newline at end of file diff --git a/OnlyPrompt.Frontend/login.html b/OnlyPrompt.Frontend/login.html index bc64173..7432682 100644 --- a/OnlyPrompt.Frontend/login.html +++ b/OnlyPrompt.Frontend/login.html @@ -63,12 +63,12 @@ - + diff --git a/OnlyPrompt.Frontend/signup.html b/OnlyPrompt.Frontend/signup.html index 24375ee..b2d726f 100644 --- a/OnlyPrompt.Frontend/signup.html +++ b/OnlyPrompt.Frontend/signup.html @@ -5,7 +5,8 @@ - + + @@ -19,8 +20,8 @@ - -
+ +
- - + + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 844fc1b..ee25dcc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,17 @@ services: onlyprompt.backend: - image: ${DOCKER_REGISTRY-}onlyprompt - build: - context: . - dockerfile: OnlyPrompt.Backend/Dockerfile - ports: - - "${PORT_PREFIX}1:8080" - - "${PORT_PREFIX}2:8081" - environment: - JWT__ISSUER: "https://onlyprompt.com" - CONNECTIONSTRINGS__DEFAULT: "Include Error Detail=true;User ID=${DB_USER};Password=${DB_PASSWORD};Host=postgres;Port=5432;Database=${DB_NAME};Pooling=true;MinPoolSize=0;MaxPoolSize=100;Connection Lifetime=0;" - ASPNETCORE_URLS: "http://*:8080" - ASPNETCORE_ENVIRONMENT: "Development" + image: ${DOCKER_REGISTRY-}onlypromptbackend + restart: unless-stopped + build: + context: . + dockerfile: OnlyPrompt.Backend/Dockerfile + ports: + - "${PORT_PREFIX}1:8080" + - "${PORT_PREFIX}2:8081" + environment: + JWT__ISSUER: "https://onlyprompt.com" + CONNECTIONSTRINGS__DEFAULTCONNECTION: "Include Error Detail=true;User ID=${DB_USER};Password=${DB_PASSWORD};Host=database;Port=5432;Database=${DB_NAME};Pooling=true;MinPoolSize=0;MaxPoolSize=100;Connection Lifetime=0;" + ASPNETCORE_URLS: "http://*:8080" database: image: postgres:latest