- register and login working

This commit is contained in:
Aeolin Ferjünnoz 2026-04-12 17:14:52 +02:00
parent 6abc36bc9b
commit 806ad23315
8 changed files with 86 additions and 75 deletions

View File

@ -4,5 +4,5 @@ using System.ComponentModel.DataAnnotations;
namespace OnlyPrompt.Backend.ApiModels.Auth namespace OnlyPrompt.Backend.ApiModels.Auth
{ {
public record ApiLoginRequest(string UserNameOrEmail, string Password); 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);
} }

View File

@ -1,10 +1,22 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Reflection.Metadata.Ecma335;
namespace OnlyPrompt.Backend.ApiModels.Validators namespace OnlyPrompt.Backend.ApiModels.Validators
{ {
public class NoWhitespaceAttribute : ValidationAttribute 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) protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{ {
if(value is string strValue) if(value is string strValue)
@ -15,7 +27,7 @@ namespace OnlyPrompt.Backend.ApiModels.Validators
return ValidationResult.Success; 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.
} }
} }

View File

@ -54,7 +54,7 @@ namespace OnlyPrompt.Backend.Controllers
[AllowAnonymous] [AllowAnonymous]
[HttpPost("register")] [HttpPost("register")]
public async Task<Results<RedirectHttpResult, ValidationProblem, Ok<ApiUser>>> RegisterAsync([FromBody] ApiRegisterRequest request) public async Task<Results<RedirectHttpResult, ValidationProblem, Ok<ApiUser>>> RegisterAsync([FromBody] ApiRegisterRequest request, [FromQuery] string redirect = null)
{ {
var existingUser = await FindUserAsync(request.UserName, request.Email); var existingUser = await FindUserAsync(request.UserName, request.Email);
if (existingUser is not null) if (existingUser is not null)
@ -84,7 +84,7 @@ namespace OnlyPrompt.Backend.Controllers
}, },
Roles = [ModelConstants.UserRole], Roles = [ModelConstants.UserRole],
PasswordHash = null, PasswordHash = null,
UserName = request.UserName, UserName = request.UserName ?? request.Email,
Email = request.Email, Email = request.Email,
IsLockoutEnabled = false, IsLockoutEnabled = false,
}; };
@ -92,6 +92,9 @@ namespace OnlyPrompt.Backend.Controllers
newUser.PasswordHash = _passwordHasher.HashPassword(newUser, request.Password); newUser.PasswordHash = _passwordHasher.HashPassword(newUser, request.Password);
_db.Users.Add(newUser); _db.Users.Add(newUser);
await _db.SaveChangesAsync(); await _db.SaveChangesAsync();
if(string.IsNullOrEmpty(redirect) == false)
return TypedResults.Redirect(redirect, false);
return TypedResults.Ok(_mapper.Map<ApiUser>(newUser)); return TypedResults.Ok(_mapper.Map<ApiUser>(newUser));
} }

View File

@ -32,6 +32,11 @@ export async function sendFormAsync(form, url, method) {
method = method || form.method || 'post'; method = method || form.method || 'post';
const data = formToObject(form); const data = formToObject(form);
const response = await sendJsonAsync(url, data, method); const response = await sendJsonAsync(url, data, method);
if (response.ok && response.redirected) {
window.location.href = response.url;
return null;
}
const responseText = await response.text(); const responseText = await response.text();
if (response.ok == false && handleValidationError(response, responseText, form)) { if (response.ok == false && handleValidationError(response, responseText, form)) {
return null; return null;
@ -41,11 +46,6 @@ export async function sendFormAsync(form, url, method) {
handleGenericFormError(response, responseText, form); handleGenericFormError(response, responseText, form);
return null; return null;
} else { } else {
if(response.redirected){
window.location.href = response.url;
return null;
}
return responseText.length == 0 ? null : JSON.parse(responseText); return responseText.length == 0 ? null : JSON.parse(responseText);
} }
} }
@ -115,13 +115,25 @@ const unknownInputErrorTemplate = new Template(`
</div> </div>
`); `);
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) { function handleValidationError(response, responseText, form) {
if (response.status !== 400) return false; if (response.status !== 400) return false;
const responseObject = JSON.parse(responseText); const responseObject = JSON.parse(responseText);
const unknownInputErrors = {}; const unknownInputErrors = {};
if (responseObject.type === 'https://tools.ietf.org/html/rfc9110#section-15.5.1' && responseObject.errors) { if (responseObject.type === 'https://tools.ietf.org/html/rfc9110#section-15.5.1' && responseObject.errors) {
for (const [field, messages] of Object.entries(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) { if (input) {
const parent = input.parentElement; const parent = input.parentElement;
const errorHtml = validationErrorTemplate.render(messages); const errorHtml = validationErrorTemplate.render(messages);

View File

@ -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();
});

View File

@ -63,12 +63,12 @@
</div> </div>
</div> </div>
<button type="submit" class="login-button">Log In</button> <button type="submit" id="login-button" class="login-button">Log In</button>
</form> </form>
<p class="signup-text"> <p class="signup-text">
Don't have an account? Don't have an account?
<a href="#">Sign Up</a> <a href="/signup">Sign Up</a>
</p> </p>
</section> </section>
</main> </main>

View File

@ -5,7 +5,8 @@
<html lang="en"> <html lang="en">
<!--Info about page but not visible--> <!--Info about page but not visible-->
<head>
<head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<!-- For responsive design: adapts width for different devices --> <!-- For responsive design: adapts width for different devices -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -19,8 +20,8 @@
</head> </head>
<body> <body>
<!-- Main container for the login page (CSS layout) --> <!-- Main container for the login page (CSS layout) -->
<main class="login-page"> <main class="login-page">
<!-- White login card --> <!-- White login card -->
<section class="login-card"> <section class="login-card">
<!-- Logo container --> <!-- Logo container -->
@ -30,69 +31,39 @@
<h1 class="login-title">Sign Up</h1> <h1 class="login-title">Sign Up</h1>
<p class="login-subtitle">Create your account to get started.</p> <p class="login-subtitle">Create your account to get started.</p>
<!-- Login form, id is used for JavaScript validation --> <!-- Login form, id is used for JavaScript validation -->
<form id="loginForm" class="login-form"> <form id="signupForm" class="login-form" action="/api/v1/auth/register?redirect=/login" method="post">
<div class="form-group"> <div class="form-group">
<label for="email">Email Address</label> <label for="email">Email Address</label>
<input <input type="email" id="email" name="email" placeholder="yourname@email.com" required>
type="email" </div>
id="email"
name="email" <div class="form-group">
placeholder="yourname@email.com" <label for="displayName">Display Name (how it will appear to others)</label>
required <input type="text" id="displayName" name="displayName" placeholder="Enter your display name" required>
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>
<!-- Password field with button to show/hide password --> <!-- Password field with button to show/hide password -->
<div class="password-wrapper"> <div class="password-wrapper">
<input <input type="password" id="password" name="password" placeholder="Enter your password" required>
type="password"
id="password"
name="password"
placeholder="Enter your password"
required
>
<button type="button" id="togglePassword" class="toggle-password"> <button type="button" id="togglePassword" class="toggle-password">
Show <!-- Click to show/hide password --> Show <!-- Click to show/hide password -->
</button> </button>
</div> </div>
</div> </div>
<div class="form-group">
<label for="email">Full Name</label>
<input
type="text"
id="fullName"
name="fullName"
placeholder="Enter your full name"
required
>
</div>
<div class="form-group">
<label for="email">Username</label>
<input
type="text"
id="username"
name="username"
placeholder="Enter your username"
required
>
</div>
<p class="signup-terms"> <p class="signup-terms">
By signing up, you agree to our By signing up, you agree to our
<a href="#">Terms</a>, <a href="#">Terms</a>,
<a href="#">Privacy Policy</a> and <a href="#">Privacy Policy</a> and
<a href="#">Cookies Policy</a>. <a href="#">Cookies Policy</a>.
</p> </p>
<button type="submit" class="login-button">Sign Up</button> <button type="submit" id="signup-button" class="login-button">Sign Up</button>
</form> </form>
<p class="signup-text"> <p class="signup-text">
@ -102,7 +73,8 @@
</section> </section>
</main> </main>
<!-- Script for login logic and form validation --> <!-- Script for signup logic and form validation -->
<script src="../js/login.js"></script> <script type="module" src="js/signup.js"></script>
</body> </body>
</html> </html>

View File

@ -1,17 +1,17 @@
services: services:
onlyprompt.backend: onlyprompt.backend:
image: ${DOCKER_REGISTRY-}onlyprompt image: ${DOCKER_REGISTRY-}onlypromptbackend
build: restart: unless-stopped
context: . build:
dockerfile: OnlyPrompt.Backend/Dockerfile context: .
ports: dockerfile: OnlyPrompt.Backend/Dockerfile
- "${PORT_PREFIX}1:8080" ports:
- "${PORT_PREFIX}2:8081" - "${PORT_PREFIX}1:8080"
environment: - "${PORT_PREFIX}2:8081"
JWT__ISSUER: "https://onlyprompt.com" environment:
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;" JWT__ISSUER: "https://onlyprompt.com"
ASPNETCORE_URLS: "http://*:8080" 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_ENVIRONMENT: "Development" ASPNETCORE_URLS: "http://*:8080"
database: database:
image: postgres:latest image: postgres:latest