- register and login working
This commit is contained in:
parent
6abc36bc9b
commit
806ad23315
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
12
OnlyPrompt.Frontend/js/signup.js
Normal file
12
OnlyPrompt.Frontend/js/signup.js
Normal 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();
|
||||||
|
});
|
||||||
@ -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>
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
<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 -->
|
||||||
@ -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 -->
|
||||||
@ -32,67 +33,37 @@
|
|||||||
<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" id="signup-button" class="login-button">Sign Up</button>
|
||||||
<button type="submit" 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>
|
||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user