Skip to content

Commit

Permalink
add turnstile to login and register
Browse files Browse the repository at this point in the history
  • Loading branch information
cankurttekin committed Nov 12, 2024
1 parent 3786bd4 commit b7a806f
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 17 deletions.
1 change: 1 addition & 0 deletions backend/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ services:
MAIL_PORT: ${MAIL_PORT}
MAIL_USER: ${MAIL_USER}
MAIL_PASS: ${MAIL_PASS}
TURNSTILE_SECRET: ${TURNSTILE_SECRET}

depends_on:
- db
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.kurttekin.can.job_track.application.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
@Service
public class TurnstileVerificationService {
@Value("${turnstile.secret-key}")
private String turnstileSecretKey;
private static final String VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
public boolean verifyToken(String turnstileToken) {
RestTemplate restTemplate = new RestTemplate();
Map<String, String> requestBody = new HashMap<>();
requestBody.put("secret", turnstileSecretKey);
requestBody.put("response", turnstileToken);
Map response = restTemplate.postForObject(VERIFY_URL, requestBody, Map.class);
return response != null && Boolean.TRUE.equals(response.get("success"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.kurttekin.can.job_track.application.dto.ErrorResponse;
import com.kurttekin.can.job_track.application.dto.UserRegistrationRequest;
import com.kurttekin.can.job_track.application.service.TurnstileVerificationService;
import com.kurttekin.can.job_track.domain.model.user.User;
import com.kurttekin.can.job_track.domain.service.UserService;
import com.kurttekin.can.job_track.domain.service.VerificationService;
Expand Down Expand Up @@ -34,9 +35,19 @@ public class AuthController {
@Autowired
private VerificationService verificationService;

@Autowired
private TurnstileVerificationService turnstileVerificationService;

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest,
@RequestParam String turnstileToken) {
try {
// Verify Turnstile token
boolean isTokenValid = turnstileVerificationService.verifyToken(turnstileToken);
if (!isTokenValid) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse("CAPTCHA failed."));
}

User user = userService.findUserByUsername(loginRequest.getUsername())
.orElseThrow(() -> new BadCredentialsException("Invalid credentials"));

Expand Down Expand Up @@ -64,8 +75,14 @@ public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
}

@PostMapping("/register")
public ResponseEntity<String> registerUser(@RequestBody UserRegistrationRequest userRequest) {
public ResponseEntity<String> registerUser(@RequestBody UserRegistrationRequest userRequest,
@RequestParam String turnstileToken) {
try {
// Verify Turnstile token
boolean isTokenValid = turnstileVerificationService.verifyToken(turnstileToken);
if (!isTokenValid) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("CAPTCHA failed.");
}
userService.registerUser(userRequest);
return ResponseEntity.ok("User registered successfully! Please verify your email before logging in.");
} catch (Exception e) {
Expand All @@ -78,4 +95,4 @@ public String verifyEmail(@RequestParam("token") String token) {
boolean isValid = verificationService.verifyUser(token);
return isValid ? "Email verified successfully. You can now log in." : "Invalid or expired verification token.";
}
}
}
2 changes: 2 additions & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ server.port=${PORT:8080}
#server.ssl.key-store-password=${KEY_STORE_PASS}
#server.ssl.keyStoreType=PKCS12
#server.ssl.key-alias=${KEY_STORE_ALIAS}

turnstile.secret-key=${TURNSTILE_SECRET:"0x0000"}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.kurttekin.can.job_track.application.dto.LoginRequest;
import com.kurttekin.can.job_track.application.dto.UserRegistrationRequest;
import com.kurttekin.can.job_track.application.service.EmailService;
import com.kurttekin.can.job_track.application.service.TurnstileVerificationService;
import com.kurttekin.can.job_track.domain.service.UserService;
import com.kurttekin.can.job_track.infrastructure.security.jwt.JwtProvider;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -48,10 +49,13 @@ class AuthControllerTest {
@Mock
private EmailService emailService;

@Mock
private TurnstileVerificationService turnstileVerificationService;

private LoginRequest loginRequest;
private UserRegistrationRequest userRegistrationRequest;
private String token;

private String turnstileToken;

@BeforeEach
public void setUp() {
Expand All @@ -60,14 +64,19 @@ public void setUp() {

userRegistrationRequest = new UserRegistrationRequest("testuser", "[email protected]", "testpassword");
token = "test.jwt.token";
turnstileToken= "test.jwt.turnstile";
}

@Test
public void testLogin_InvalidCredentials() {
// Mock Turnstile verification logic
//when(turnstileVerificationService.verifyToken(anyString())).thenReturn(true);
when(turnstileVerificationService.verifyToken(turnstileToken)).thenReturn(true);

when(authenticationManager.authenticate(any(UsernamePasswordAuthenticationToken.class)))
.thenThrow(new BadCredentialsException("Invalid credentials"));

ResponseEntity<?> response = authController.login(loginRequest);
ResponseEntity<?> response = authController.login(loginRequest, turnstileToken);

assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
assertEquals("Invalid credentials", response.getBody());
Expand All @@ -82,8 +91,10 @@ public void testRegisterUser_Success() {
doNothing().when(userService).registerUser(any(UserRegistrationRequest.class));
doNothing().when(emailService).sendVerificationEmail(anyString(),anyString(), anyString()); // Mock email sending

when(turnstileVerificationService.verifyToken(turnstileToken)).thenReturn(true);

// Call the registerUser method in the controller
ResponseEntity<String> response = authController.registerUser(userRegistrationRequest);
ResponseEntity<String> response = authController.registerUser(userRegistrationRequest, turnstileToken);

// Check the status and response body
assertEquals(HttpStatus.OK, response.getStatusCode());
Expand All @@ -94,7 +105,9 @@ public void testRegisterUser_Success() {
public void testRegisterUser_Failure() {
doThrow(new RuntimeException("Registration failed")).when(userService).registerUser(any(UserRegistrationRequest.class));

ResponseEntity<String> response = authController.registerUser(userRegistrationRequest);
when(turnstileVerificationService.verifyToken(turnstileToken)).thenReturn(true);

ResponseEntity<String> response = authController.registerUser(userRegistrationRequest, turnstileToken);

assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertEquals("Registration failed", response.getBody());
Expand Down
11 changes: 11 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"react-router-dom": "^6.27.0",
"react-scripts": "^5.0.1",
"react-toastify": "^10.0.5",
"react-turnstile": "^1.1.4",
"recharts": "^2.13.0",
"styled-components": "^6.1.13",
"web-vitals": "^2.1.4"
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useContext, useState } from 'react';
import styled from 'styled-components';
import { useNavigate } from 'react-router-dom';
import { AuthContext } from '../contexts/AuthContext'; // Import the context
import TurnstileWidget from './TurnstileWidget';

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -50,6 +51,11 @@ const Login = () => {
const [password, setPassword] = useState("");
const [error, setError] = useState(""); // To store and display error messages
const { login } = useContext(AuthContext);
const [turnstileToken, setTurnstileToken] = useState(null);

const handleTurnstileChange = (token) => {
setTurnstileToken(token);
};

const handleSubmit = async (e) => {
e.preventDefault();
Expand All @@ -61,8 +67,13 @@ const Login = () => {
return;
}

if (!turnstileToken) {
setError("Please complete the CAPTCHA.");
return;
}

try {
await login(username, password); // Call the login function
await login(username, password, turnstileToken); // Call the login function
navigate('/job-applications'); // Redirect after successful login
} catch (error) {
setError(error.message); // Set the error message
Expand All @@ -87,6 +98,7 @@ const Login = () => {
value={password}
onChange={e => setPassword(e.target.value)}
/>
<TurnstileWidget onChange={handleTurnstileChange} />
<Button type="submit">Login</Button>
{error && <Error>{error}</Error>} {/* Display error message */}
</form>
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/components/Register.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';
import { register as registerService } from '../services/authService'; // Import the register function from authService
import TurnstileWidget from './TurnstileWidget';

// Styled components for layout and styling
const Container = styled.div`
Expand Down Expand Up @@ -75,7 +76,7 @@ const Register = () => {
specialChar: false,
});
const [isPasswordFocused, setIsPasswordFocused] = useState(false); // Track if the password field is focused

const [turnstileToken, setTurnstileToken] = useState(null);
const navigate = useNavigate();

// Password validation function
Expand Down Expand Up @@ -107,6 +108,10 @@ const Register = () => {
setIsPasswordFocused(false);
};

const handleTurnstileChange = (token) => {
setTurnstileToken(token);
};

// Handle form submit
const handleSubmit = async (e) => {
e.preventDefault();
Expand All @@ -124,8 +129,13 @@ const Register = () => {
return;
}

if (!turnstileToken) {
setError("Please complete the CAPTCHA.");
return;
}

try {
await registerService(username, email, password); // Call the register function
await registerService(username, email, password, turnstileToken); // Call the register function
setError('Registration successful. Please verify your email before logging in.');
setTimeout(() => navigate('/login'), 1500); // Redirect after 1.5 seconds
} catch (err) {
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/TurnstileWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, { useState } from 'react';
import Turnstile from 'react-turnstile';
import {REACT_APP_TURNSTILE_SITE_KEY} from "../config";

const TurnstileWidget = ({ onChange }) => {
const [token, setToken] = useState(null);

const handleTurnstileChange = (value) => {
setToken(value);
if (onChange) {
onChange(value); // Pass token back to parent component
}
};

return (
<div>
<Turnstile
sitekey={REACT_APP_TURNSTILE_SITE_KEY}
onChange={handleTurnstileChange}
/>
</div>
);
};

export default TurnstileWidget;
3 changes: 2 additions & 1 deletion frontend/src/config.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const REACT_APP_BACKEND_URL = process.env.REACT_APP_BACKEND_URL+'/api';
export const REACT_APP_BACKEND_URL = process.env.REACT_APP_BACKEND_URL+'/api';
export const REACT_APP_TURNSTILE_SITE_KEY = process.env.REACT_APP_TURNSTILE_SITE_KEY;
4 changes: 2 additions & 2 deletions frontend/src/contexts/AuthContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export const AuthProvider = ({ children }) => {
}
}, []);

const login = async (username, password) => {
const login = async (username, password, turnstileToken) => {
try {
const response = await loginService(username, password);
const response = await loginService(username, password, turnstileToken);
const { token } = response;

if (token) {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/services/authService.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import axios from 'axios';
import { REACT_APP_BACKEND_URL } from '../config';

export const login = async (username, password) => {
export const login = async (username, password, turnstileToken) => {
try {
const response = await axios.post(
`${REACT_APP_BACKEND_URL}/auth/login`,
{ username, password },
{ username, password, turnstileToken },
{
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -33,8 +33,8 @@ export const login = async (username, password) => {
}
};

export const register = async (username, email, password) => {
return axios.post(`${REACT_APP_BACKEND_URL}/auth/register`, { username, email, password });
export const register = async (username, email, password, turnstileToken) => {
return axios.post(`${REACT_APP_BACKEND_URL}/auth/register`, { username, email, password, turnstileToken });
};

export const logout = () => {
Expand Down

0 comments on commit b7a806f

Please sign in to comment.