Usun symulowane logowanie przez Google i Facebook
Deploy Polska Lokalnie / deploy (push) Successful in 30s

Endpoint /api/auth/social byl atrapa bez prawdziwego OAuth: wydawal token
sesji dla dowolnego adresu e-mail podanego w zadaniu, bez hasla. Podanie
adresu administratora dawalo pelna sesje z rola ADMIN.

Na tym etapie logowanie ma byc wylacznie dla uzytkownikow systemu (e-mail
i haslo), wiec usuwam je w calosci:

- backend: endpoint, wpis permitAll w SecurityConfig, AuthService.socialLogin
  z pomocnikami i DTO SocialLoginRequest. Enum AuthProvider zostaje, bo
  istniejace konta moga miec GOOGLE/FACEBOOK.
- frontend: przyciski Google/Facebook w formularzach logowania i rejestracji,
  socialLogin w AuthContext oraz osierocone style.

Test regresyjny AuthControllerTest nie uruchomi sie jeszcze na H2 - start
kontekstu wywraca sie na split_part w SQL naprawiajacym nicki (ten sam blad
dotyczy istniejacego contextLoads). Zweryfikowane recznie na stacku
z PostgreSQL: /api/auth/social -> 403, logowanie e-mailem -> 200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-21 14:20:32 +02:00
parent a3e4505194
commit 4453ef2305
8 changed files with 54 additions and 210 deletions
@@ -23,7 +23,6 @@ import pl.polskalokalnie.auth.dto.AuthResponse;
import pl.polskalokalnie.auth.dto.LoginRequest;
import pl.polskalokalnie.auth.dto.RegisterRequest;
import pl.polskalokalnie.auth.dto.RegisterResponse;
import pl.polskalokalnie.auth.dto.SocialLoginRequest;
import pl.polskalokalnie.auth.dto.UpdateProfileRequest;
import pl.polskalokalnie.auth.dto.UserResponse;
@@ -47,11 +46,6 @@ public class AuthController {
return authService.login(request);
}
@PostMapping("/social")
public AuthResponse social(@Valid @RequestBody SocialLoginRequest request) {
return authService.socialLogin(request);
}
// --- Aktywacja konta ---
@PostMapping("/activate")
@@ -19,7 +19,6 @@ import pl.polskalokalnie.auth.dto.AuthResponse;
import pl.polskalokalnie.auth.dto.LoginRequest;
import pl.polskalokalnie.auth.dto.RegisterRequest;
import pl.polskalokalnie.auth.dto.RegisterResponse;
import pl.polskalokalnie.auth.dto.SocialLoginRequest;
import pl.polskalokalnie.auth.dto.UpdateProfileRequest;
import pl.polskalokalnie.auth.dto.UserResponse;
import pl.polskalokalnie.user.AccountType;
@@ -261,45 +260,6 @@ public class AuthService {
return authToken;
}
/**
* Symulowane logowanie spoleczne: znajduje lub tworzy konto powiazane z dostawca.
* Nie ma tu prawdziwego OAuth - dane przychodza z frontendu jako demo.
*/
public AuthResponse socialLogin(SocialLoginRequest request) {
AuthProvider provider = request.provider();
String email = normalizeEmail(
request.email() != null && !request.email().isBlank()
? request.email()
: defaultSocialEmail(provider));
String name = request.fullName() != null && !request.fullName().isBlank()
? request.fullName().trim()
: defaultSocialName(provider);
textModerationService.validateOrThrow(name);
AppUser user = userRepository.findByEmailIgnoreCase(email).orElseGet(() -> {
if (blockedEmailRepository.existsByEmailIgnoreCase(email)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Ten adres e-mail został zablokowany i nie można go już użyć do rejestracji");
}
AppUser created = new AppUser();
created.setEmail(email);
created.setFullName(name);
created.setNick(nickService.generateFromName(name, email, null));
created.setRole(Role.USER);
created.setProvider(provider);
// Logowanie spoleczne nie wymaga rekopisania danych, wiec konto jest od razu zweryfikowane.
created.setVerified(true);
return userRepository.save(created);
});
if (user.isBlocked()) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Konto zostało zablokowane");
}
leadSyncService.syncUser(user);
return buildAuthResponse(user);
}
// Odstep, po ktorym wejscie do aplikacji (odswiezenie /auth/me) na nowo odnotowuje aktywnosc.
// Bez tego zapisywalibysmy do bazy przy kazdym zadaniu profilu - a wystarczy zgrubna dokladnosc.
private static final Duration LAST_ACTIVE_REFRESH = Duration.ofMinutes(2);
@@ -491,20 +451,4 @@ public class AuthService {
String source = fullName != null && !fullName.isBlank() ? fullName : user.getFullName();
return nickService.generateFromName(source, user.getEmail(), user);
}
private String defaultSocialEmail(AuthProvider provider) {
return switch (provider) {
case GOOGLE -> "demo.google@gmail.com";
case FACEBOOK -> "demo.facebook@facebook.com";
case LOCAL -> "demo.local@polskalokalnie.pl";
};
}
private String defaultSocialName(AuthProvider provider) {
return switch (provider) {
case GOOGLE -> "Użytkownik Google";
case FACEBOOK -> "Użytkownik Facebook";
case LOCAL -> "Użytkownik";
};
}
}
@@ -35,7 +35,7 @@ public class SecurityConfig {
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/error").permitAll()
.requestMatchers("/api/auth/register", "/api/auth/login", "/api/auth/social").permitAll()
.requestMatchers("/api/auth/register", "/api/auth/login").permitAll()
.requestMatchers("/api/auth/activate", "/api/auth/resend-activation",
"/api/auth/forgot-password", "/api/auth/reset-password",
"/api/auth/verify-phone", "/api/auth/resend-phone-otp").permitAll()
@@ -1,15 +0,0 @@
package pl.polskalokalnie.auth.dto;
import jakarta.validation.constraints.NotNull;
import pl.polskalokalnie.user.AuthProvider;
/**
* Symulowane logowanie spoleczne. W wersji produkcyjnej email/name pochodzilyby
* z tokenu dostawcy OAuth (Google/Facebook); tutaj przychodza z frontendu jako dane demo.
*/
public record SocialLoginRequest(
@NotNull AuthProvider provider,
String email,
String fullName
) {
}
@@ -0,0 +1,44 @@
package pl.polskalokalnie.auth;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
// Regresja: symulowane logowanie spoleczne wydawalo token dla dowolnego adresu e-mail
// bez hasla - wystarczylo podac adres administratora, zeby dostac jego sesje.
@Test
void socialLoginEndpointDoesNotIssueToken() throws Exception {
mockMvc.perform(post("/api/auth/social")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"provider\":\"GOOGLE\",\"email\":\"admin@softspm.com\"}"))
.andExpect(status().is4xxClientError())
.andExpect(content().string(not(containsString("token"))));
}
@Test
void adminCanLogInWithEmailAndPassword() throws Exception {
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"admin@softspm.com\",\"password\":\"Admin123!\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").isNotEmpty())
.andExpect(jsonPath("$.user.role").value("ADMIN"));
}
}
+6 -55
View File
@@ -12984,28 +12984,8 @@ type LoginStep = 'login' | 'register-choice' | 'register-private' | 'register-co
// Dane firmy z rejestru REGON (GUS) - odpowiedz /api/gus/company.
type GusCompany = { name: string | null; nip: string; regon: string | null; address: string | null };
// Oficjalne logotypy dostawcow logowania spolecznosciowego (SVG w barwach marki).
function GoogleIcon() {
return (
<svg className="social-login-icon" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">
<path fill="#4285F4" d="M17.64 9.205c0-.639-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.796 2.716v2.259h2.908c1.702-1.567 2.684-3.875 2.684-6.615z" />
<path fill="#34A853" d="M9 18c2.43 0 4.467-.806 5.956-2.18l-2.908-2.259c-.806.54-1.837.86-3.048.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 0 0 9 18z" />
<path fill="#FBBC05" d="M3.964 10.71A5.41 5.41 0 0 1 3.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 0 0 0 9c0 1.452.348 2.827.957 4.042l3.007-2.332z" />
<path fill="#EA4335" d="M9 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.581C13.463.891 11.426 0 9 0A8.997 8.997 0 0 0 .957 4.958L3.964 7.29C4.672 5.163 6.656 3.58 9 3.58z" />
</svg>
);
}
function FacebookIcon() {
return (
<svg className="social-login-icon" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
<path fill="#1877F2" d="M24 12.073C24 5.404 18.627 0 12 0S0 5.404 0 12.073c0 6.026 4.388 11.02 10.125 11.926v-8.437H7.078v-3.49h3.047V9.41c0-3.017 1.792-4.686 4.533-4.686 1.312 0 2.686.235 2.686.235v2.97h-1.513c-1.49 0-1.955.93-1.955 1.886v2.259h3.328l-.532 3.489h-2.796v8.437C19.612 23.094 24 18.1 24 12.073z" />
</svg>
);
}
function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
const { login, register, socialLogin, forgotPassword, resendActivation } = useAuth();
const { login, register, forgotPassword, resendActivation } = useAuth();
const [step, setStep] = useState<LoginStep>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
@@ -13181,37 +13161,8 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
}
};
const socialButtons = (
<>
<div className="login-divider">
<span>lub kontynuuj z</span>
</div>
<div className="social-login-row">
<button
className="social-login"
type="button"
disabled={submitting}
onClick={() => runAuth(() => socialLogin('GOOGLE'))}
aria-label="Kontynuuj z Google"
>
<GoogleIcon />
<span>Google</span>
</button>
<button
className="social-login"
type="button"
disabled={submitting}
onClick={() => runAuth(() => socialLogin('FACEBOOK'))}
aria-label="Kontynuuj z Facebookiem"
>
<FacebookIcon />
<span>Facebook</span>
</button>
</div>
<p className="login-security-note"><Icon name="lock" /> Twoje dane są u nas bezpieczne. Stosujemy szyfrowanie SSL.</p>
</>
const securityNote = (
<p className="login-security-note"><Icon name="lock" /> Twoje dane są u nas bezpieczne. Stosujemy szyfrowanie SSL.</p>
);
return (
@@ -13326,7 +13277,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
{submitting ? 'Przetwarzanie…' : 'Zaloguj się'}
</button>
{socialButtons}
{securityNote}
</form>
)}
@@ -13493,7 +13444,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
{submitting ? 'Przetwarzanie…' : 'Zarejestruj się'}
</button>
{socialButtons}
{securityNote}
</form>
)}
@@ -13650,7 +13601,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) {
{submitting ? 'Przetwarzanie…' : 'Zarejestruj firmę'}
</button>
{socialButtons}
{securityNote}
</form>
)}
+2 -12
View File
@@ -104,7 +104,6 @@ type AuthContextValue = {
loading: boolean;
login: (email: string, password: string) => Promise<AuthUser>;
register: (email: string, password: string, fullName: string, details?: RegisterDetails) => Promise<RegisterPending>;
socialLogin: (provider: Exclude<AuthProviderName, 'LOCAL'>) => Promise<AuthUser>;
activateAccount: (token: string) => Promise<void>;
resendActivation: (email: string) => Promise<void>;
forgotPassword: (email: string) => Promise<void>;
@@ -266,15 +265,6 @@ export function AuthProvider({ children }: { children: ReactNode }) {
[],
);
const socialLogin = useCallback(
async (provider: Exclude<AuthProviderName, 'LOCAL'>) =>
applyAuth(await apiFetch<AuthResponse>('/auth/social', {
method: 'POST',
body: JSON.stringify({ provider }),
})),
[applyAuth],
);
const activateAccount = useCallback(async (token: string) => {
await apiFetch<void>('/auth/activate', { method: 'POST', body: JSON.stringify({ token }) });
}, []);
@@ -372,11 +362,11 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const value = useMemo<AuthContextValue>(
() => ({
user, token, loading, login, register, socialLogin,
user, token, loading, login, register,
activateAccount, resendActivation, forgotPassword, resetPassword, verifyPhone, resendPhoneOtp,
updateProfile, changePassword, verifyPassword, passwordCooldown, deleteAccount, loadSettings, loadSearchPreferences, saveSettings, refreshUser, logout,
}),
[user, token, loading, login, register, socialLogin,
[user, token, loading, login, register,
activateAccount, resendActivation, forgotPassword, resetPassword, verifyPhone, resendPhoneOtp,
updateProfile, changePassword, verifyPassword, passwordCooldown, deleteAccount, loadSettings, loadSearchPreferences, saveSettings, refreshUser, logout],
);
+1 -65
View File
@@ -13604,67 +13604,6 @@ a.city-card:hover {
width: 100%;
}
.login-divider {
align-items: center;
color: #6e7d92;
display: grid;
font-size: 13px;
font-weight: 850;
grid-template-columns: 1fr auto 1fr;
gap: 9px;
}
.login-divider::before,
.login-divider::after {
background: #dfe5ed;
content: "";
height: 1px;
}
.social-login-row {
display: flex;
gap: 10px;
}
.social-login {
align-items: center;
background: #ffffff;
border: 1px solid #dce4ec;
border-radius: 10px;
color: #24344b;
cursor: pointer;
display: flex;
flex: 1 1 0;
font-size: 14px;
font-weight: 800;
gap: 9px;
height: 46px;
justify-content: center;
min-width: 0;
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}
.social-login:hover:not(:disabled) {
background: #f7f9fc;
border-color: #c4d2e2;
box-shadow: 0 2px 10px rgba(20, 34, 58, 0.07);
}
.social-login:disabled {
cursor: default;
opacity: 0.6;
}
.social-login-icon {
flex: 0 0 auto;
height: 18px;
width: 18px;
}
@media (max-width: 400px) {
.social-login-row { flex-direction: column; }
}
.login-security-note {
align-items: center;
color: #7a8798;
@@ -14018,8 +13957,6 @@ a.city-card:hover {
.remember-me,
.forgot-password,
.login-submit,
.social-login,
.login-divider,
.login-security-note {
font-size: 13px;
}
@@ -14035,8 +13972,7 @@ a.city-card:hover {
}
.login-field input,
.login-submit,
.social-login {
.login-submit {
height: 44px;
}