login fix
This commit is contained in:
parent
bbb79c25e2
commit
7ffbe04e37
@ -10,6 +10,8 @@ enum AppState: Equatable, Hashable {
|
|||||||
return true
|
return true
|
||||||
case (.login, .login):
|
case (.login, .login):
|
||||||
return true
|
return true
|
||||||
|
case (.signup, .signup):
|
||||||
|
return true
|
||||||
case (.dashboard, .dashboard):
|
case (.dashboard, .dashboard):
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@ -20,5 +22,6 @@ enum AppState: Equatable, Hashable {
|
|||||||
|
|
||||||
case splashScreen
|
case splashScreen
|
||||||
case login
|
case login
|
||||||
|
case signup
|
||||||
case dashboard
|
case dashboard
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,10 @@ final class LoginViewModel: ViewModel {
|
|||||||
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !password.isEmpty && !isLoading
|
!email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !password.isEmpty && !isLoading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showSignup() {
|
||||||
|
appStateManager.navigateTo(state: .signup)
|
||||||
|
}
|
||||||
|
|
||||||
func login() async {
|
func login() async {
|
||||||
guard canSubmit else { return }
|
guard canSubmit else { return }
|
||||||
|
|
||||||
|
|||||||
59
medmap/ViewModels/SignupViewModel.swift
Normal file
59
medmap/ViewModels/SignupViewModel.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,12 +26,17 @@ struct LoginView: View {
|
|||||||
|
|
||||||
private var formPanel: some View {
|
private var formPanel: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Image("splashLogo")
|
HStack {
|
||||||
.resizable()
|
Image("splashLogo")
|
||||||
.scaledToFit()
|
.resizable()
|
||||||
.frame(width: 120, height: 76)
|
.scaledToFit()
|
||||||
.accessibilityHidden(true)
|
.frame(width: 120, height: 76)
|
||||||
.padding(.top, 32)
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 32)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -93,7 +98,9 @@ struct LoginView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
Text("Não tem conta? Criar conta")
|
Button("Não tem conta? Criar conta") {
|
||||||
|
viewModel.showSignup()
|
||||||
|
}
|
||||||
.font(.jakartaRegular14)
|
.font(.jakartaRegular14)
|
||||||
.foregroundStyle(.white.opacity(0.88))
|
.foregroundStyle(.white.opacity(0.88))
|
||||||
|
|
||||||
|
|||||||
171
medmap/Views/SignupView.swift
Normal file
171
medmap/Views/SignupView.swift
Normal 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()
|
||||||
|
}
|
||||||
@ -93,6 +93,13 @@ struct medmapApp: App {
|
|||||||
.transition(.opacity)
|
.transition(.opacity)
|
||||||
.animation(.easeInOut, value: 10)
|
.animation(.easeInOut, value: 10)
|
||||||
|
|
||||||
|
case .signup:
|
||||||
|
|
||||||
|
SignupView()
|
||||||
|
.navigationBarHidden(true)
|
||||||
|
.transition(.opacity)
|
||||||
|
.animation(.easeInOut, value: 10)
|
||||||
|
|
||||||
case .dashboard:
|
case .dashboard:
|
||||||
|
|
||||||
DashboardView()
|
DashboardView()
|
||||||
|
|||||||
@ -77,9 +77,20 @@ public class RestAuthController {
|
|||||||
})
|
})
|
||||||
@SecurityRequirements
|
@SecurityRequirements
|
||||||
@PostMapping("/signup")
|
@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);
|
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();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.set("X-User-Id", created.getId().toString());
|
headers.set("X-User-Id", created.getId().toString());
|
||||||
headers.set("X-User-Role", created.getRole().name());
|
headers.set("X-User-Role", created.getRole().name());
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user