login fix

This commit is contained in:
Ricardo Araújo 2026-09-15 16:11:48 +01:00
parent bbb79c25e2
commit 7ffbe04e37
7 changed files with 270 additions and 8 deletions

View File

@ -10,6 +10,8 @@ enum AppState: Equatable, Hashable {
return true
case (.login, .login):
return true
case (.signup, .signup):
return true
case (.dashboard, .dashboard):
return true
@ -20,5 +22,6 @@ enum AppState: Equatable, Hashable {
case splashScreen
case login
case signup
case dashboard
}

View File

@ -25,6 +25,10 @@ final class LoginViewModel: ViewModel {
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !password.isEmpty && !isLoading
}
func showSignup() {
appStateManager.navigateTo(state: .signup)
}
func login() async {
guard canSubmit else { return }

View File

@ -0,0 +1,59 @@
//
// SignupViewModel.swift
// medmap
//
import Foundation
@MainActor
final class SignupViewModel: ViewModel {
@Published var email = ""
@Published var password = ""
@Published var confirmationPassword = ""
@Published private(set) var errorMessage: String?
private let api: MedMapAPI
init(api: MedMapAPI = MedMapAPI()) {
self.api = api
super.init()
}
var canSubmit: Bool {
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
password.count >= 8 &&
password == confirmationPassword &&
!isLoading
}
func signup() async {
guard canSubmit else { return }
errorMessage = nil
isLoading = true
defer { isLoading = false }
do {
let response = try await api.signup(
email: email.trimmingCharacters(in: .whitespacesAndNewlines),
password: password,
role: .patient
)
guard response.role == .patient else {
errorMessage = "Nao foi possivel criar uma conta de doente."
return
}
appStateManager.navigateTo(state: .dashboard)
} catch {
errorMessage = "Nao foi possivel criar a conta. Confirme os dados ou tente outro email."
fluxlog("Patient signup failed: \(error.localizedDescription)")
}
}
func showLogin() {
appStateManager.navigateBack()
}
}

View File

@ -26,12 +26,17 @@ struct LoginView: View {
private var formPanel: some View {
VStack(spacing: 0) {
Image("splashLogo")
.resizable()
.scaledToFit()
.frame(width: 120, height: 76)
.accessibilityHidden(true)
.padding(.top, 32)
HStack {
Image("splashLogo")
.resizable()
.scaledToFit()
.frame(width: 120, height: 76)
.accessibilityHidden(true)
Spacer()
}
.padding(.top, 32)
.padding(.horizontal, 32)
Spacer()
@ -93,7 +98,9 @@ struct LoginView: View {
Spacer()
VStack(spacing: 8) {
Text("Não tem conta? Criar conta")
Button("Não tem conta? Criar conta") {
viewModel.showSignup()
}
.font(.jakartaRegular14)
.foregroundStyle(.white.opacity(0.88))

View File

@ -0,0 +1,171 @@
//
// SignupView.swift
// medmap
//
import SwiftUI
struct SignupView: View {
@StateObject private var viewModel = SignupViewModel()
@FocusState private var focusedField: Field?
private enum Field {
case email
case password
case confirmationPassword
}
var body: some View {
ZStack {
Color.appBackground.ignoresSafeArea()
VStack(spacing: 0) {
HStack {
Image("splashLogo")
.resizable()
.scaledToFit()
.frame(width: 120, height: 76)
.accessibilityHidden(true)
Spacer()
}
.padding(.top, 32)
.padding(.horizontal, 32)
Spacer()
VStack(alignment: .leading, spacing: 24) {
Text("Criar nova conta")
.font(.jakartaBold24)
.foregroundStyle(.white)
VStack(spacing: 14) {
field(
placeholder: "Email",
icon: "envelope",
text: $viewModel.email,
focusedField: .email,
contentType: .emailAddress,
keyboardType: .emailAddress
)
field(
placeholder: "Palavra-passe (min. 8 caracteres)",
icon: "lock",
text: $viewModel.password,
focusedField: .password,
contentType: .newPassword,
isSecure: true
)
field(
placeholder: "Confirmar palavra-passe",
icon: "lock",
text: $viewModel.confirmationPassword,
focusedField: .confirmationPassword,
contentType: .newPassword,
isSecure: true,
submitLabel: .go
) {
Task { await viewModel.signup() }
}
}
if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.font(.jakartaRegular14)
.foregroundStyle(Color.loginErrorText)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
}
Button {
focusedField = nil
Task { await viewModel.signup() }
} label: {
Group {
if viewModel.isLoading {
ProgressView().tint(Color.loginButtonText)
} else {
Text("Registar").font(.jakartaBold16)
}
}
.frame(maxWidth: .infinity)
.frame(height: 46)
}
.foregroundStyle(Color.loginButtonText)
.background(Color.loginButtonBackground)
.clipShape(RoundedRectangle(cornerRadius: 8))
.disabled(!viewModel.canSubmit)
.opacity(viewModel.canSubmit ? 1 : 0.55)
}
.padding(.horizontal, 32)
Spacer()
VStack(spacing: 8) {
Button("Ja tem conta? Entrar") {
viewModel.showLogin()
}
.font(.jakartaRegular14)
.foregroundStyle(.white.opacity(0.88))
Text("MedMap")
.font(.jakartaRegular12)
.foregroundStyle(.white.opacity(0.6))
}
.padding(.bottom, 24)
}
}
.navigationBarHidden(true)
}
private func field(
placeholder: String,
icon: String,
text: Binding<String>,
focusedField: Field,
contentType: UITextContentType,
keyboardType: UIKeyboardType = .default,
isSecure: Bool = false,
submitLabel: SubmitLabel = .next,
onSubmit: @escaping () -> Void = {}
) -> some View {
HStack(spacing: 10) {
Image(systemName: icon)
.foregroundStyle(.white)
.frame(width: 18)
ZStack(alignment: .leading) {
if text.wrappedValue.isEmpty {
Text(placeholder).foregroundStyle(Color.loginPlaceholder)
}
if isSecure {
SecureField("", text: text)
} else {
TextField("", text: text)
.keyboardType(keyboardType)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
}
}
.foregroundStyle(.white)
.tint(.white)
.font(.jakartaRegular16)
.textContentType(contentType)
.focused($focusedField, equals: focusedField)
.submitLabel(submitLabel)
.onSubmit(onSubmit)
}
.padding(.horizontal, 14)
.frame(height: 46)
.background(Color.loginFieldBackground)
.overlay {
RoundedRectangle(cornerRadius: 8)
.stroke(Color.loginFieldBorder, lineWidth: 1)
}
.clipShape(RoundedRectangle(cornerRadius: 8))
.accessibilityLabel(placeholder)
}
}
#Preview {
SignupView()
}

View File

@ -93,6 +93,13 @@ struct medmapApp: App {
.transition(.opacity)
.animation(.easeInOut, value: 10)
case .signup:
SignupView()
.navigationBarHidden(true)
.transition(.opacity)
.animation(.easeInOut, value: 10)
case .dashboard:
DashboardView()

View File

@ -77,9 +77,20 @@ public class RestAuthController {
})
@SecurityRequirements
@PostMapping("/signup")
public ResponseEntity<LoginResponse> signup(@Valid @RequestBody SignupRequest request) {
public ResponseEntity<LoginResponse> signup(@Valid @RequestBody SignupRequest request,
HttpServletRequest httpRequest) {
AppUser created = appUserService.register(request);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(new UsernamePasswordAuthenticationToken(
created,
null,
created.getAuthorities()
));
SecurityContextHolder.setContext(context);
HttpSession session = httpRequest.getSession(true);
session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);
HttpHeaders headers = new HttpHeaders();
headers.set("X-User-Id", created.getId().toString());
headers.set("X-User-Role", created.getRole().name());