- 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
{
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.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.
}
}

View File

@ -54,7 +54,7 @@ namespace OnlyPrompt.Backend.Controllers
[AllowAnonymous]
[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);
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<ApiUser>(newUser));
}

View File

@ -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(`
</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) {
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);

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>
<button type="submit" class="login-button">Log In</button>
<button type="submit" id="login-button" class="login-button">Log In</button>
</form>
<p class="signup-text">
Don't have an account?
<a href="#">Sign Up</a>
<a href="/signup">Sign Up</a>
</p>
</section>
</main>

View File

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

View File

@ -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