From 3c8288007a9745ec525ca5cce17e2fa2d908d331 Mon Sep 17 00:00:00 2001 From: Chandresh Kerkar Date: Fri, 19 Dec 2025 21:43:38 +0530 Subject: [PATCH] Working Signup --- .../example/livingai_lg/api/AuthApiClient.kt | 23 ++- .../example/livingai_lg/api/AuthManager.kt | 17 +- .../com/example/livingai_lg/api/models.kt | 34 +++- .../livingai_lg/ui/login_legacy/signup.md | 192 ++++++++++++++++++ .../ui/screens/auth/SignUpScreen.kt | 20 +- 5 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/example/livingai_lg/ui/login_legacy/signup.md diff --git a/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt b/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt index c269faf..549194d 100644 --- a/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt +++ b/app/src/main/java/com/example/livingai_lg/api/AuthApiClient.kt @@ -2,6 +2,7 @@ package com.example.livingai_lg.api import android.content.Context import android.os.Build +import android.provider.Settings import com.example.livingai_lg.BuildConfig import io.ktor.client.* import io.ktor.client.call.* @@ -11,6 +12,7 @@ import io.ktor.client.plugins.auth.* import io.ktor.client.plugins.auth.providers.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* +import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json @@ -76,13 +78,28 @@ class AuthApiClient(private val context: Context) { suspend fun verifyOtp(phoneNumber: String, code: String, deviceId: String): Result = runCatching { val response: VerifyOtpResponse = client.post("auth/verify-otp") { contentType(ContentType.Application.Json) - setBody(VerifyOtpRequest(phoneNumber, code, deviceId, getDeviceInfo())) + setBody(VerifyOtpRequest(phoneNumber, code.toInt(), deviceId, getDeviceInfo())) }.body() tokenManager.saveTokens(response.accessToken, response.refreshToken) response } + suspend fun signup(request: SignupRequest): Result = runCatching { + val response = client.post("auth/signup") { + contentType(ContentType.Application.Json) + setBody(request.copy(deviceId = getDeviceId(), deviceInfo = getDeviceInfo())) + } + + // Instead of throwing an exception on non-2xx, we return a result type + // that can be handled by the caller. + if (response.status.isSuccess()) { + response.body() + } else { + response.body() + } + } + suspend fun updateProfile(name: String, userType: String): Result = runCatching { client.put("users/me") { contentType(ContentType.Application.Json) @@ -115,4 +132,8 @@ class AuthApiClient(private val context: Context) { timezone = TimeZone.getDefault().id ) } + + private fun getDeviceId(): String { + return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) + } } diff --git a/app/src/main/java/com/example/livingai_lg/api/AuthManager.kt b/app/src/main/java/com/example/livingai_lg/api/AuthManager.kt index aabacc7..62a213e 100644 --- a/app/src/main/java/com/example/livingai_lg/api/AuthManager.kt +++ b/app/src/main/java/com/example/livingai_lg/api/AuthManager.kt @@ -16,7 +16,22 @@ class AuthManager( val deviceId = getDeviceId() return apiClient.verifyOtp(phoneNumber, code, deviceId) .onSuccess { response -> - tokenManager.saveTokens(response.accessToken, response.refreshToken) + response.accessToken?.let { accessToken -> + response.refreshToken?.let { refreshToken -> + tokenManager.saveTokens(accessToken, refreshToken) + } + } + } + } + + suspend fun signup(signupRequest: SignupRequest): Result { + return apiClient.signup(signupRequest) + .onSuccess { response -> + response.accessToken?.let { accessToken -> + response.refreshToken?.let { refreshToken -> + tokenManager.saveTokens(accessToken, refreshToken) + } + } } } diff --git a/app/src/main/java/com/example/livingai_lg/api/models.kt b/app/src/main/java/com/example/livingai_lg/api/models.kt index 3738a9d..abe17a9 100644 --- a/app/src/main/java/com/example/livingai_lg/api/models.kt +++ b/app/src/main/java/com/example/livingai_lg/api/models.kt @@ -23,7 +23,7 @@ data class DeviceInfo( @Serializable data class VerifyOtpRequest( @SerialName("phone_number") val phoneNumber: String, - val code: String, + val code: Int, @SerialName("device_id") val deviceId: String, @SerialName("device_info") val deviceInfo: DeviceInfo? = null ) @@ -37,6 +37,34 @@ data class VerifyOtpResponse( ) // endregion +// region: Signup +@Serializable +data class SignupRequest( + val name: String, + @SerialName("phone_number") val phoneNumber: String, + val state: String? = null, + val district: String? = null, + @SerialName("city_village") val cityVillage: String? = null, + @SerialName("device_id") val deviceId: String? = null, + @SerialName("device_info") val deviceInfo: DeviceInfo? = null +) + +@Serializable +data class SignupResponse( + val success: Boolean, + val user: User? = null, + @SerialName("access_token") val accessToken: String? = null, + @SerialName("refresh_token") val refreshToken: String? = null, + @SerialName("needs_profile") val needsProfile: Boolean? = null, + @SerialName("is_new_account") val isNewAccount: Boolean? = null, + @SerialName("is_new_device") val isNewDevice: Boolean? = null, + @SerialName("active_devices_count") val activeDevicesCount: Int? = null, + @SerialName("location_id") val locationId: String? = null, + val message: String? = null, + @SerialName("user_exists") val userExists: Boolean? = null +) +// endregion + // region: Token Refresh @Serializable data class RefreshRequest(@SerialName("refresh_token") val refreshToken: String) @@ -61,7 +89,9 @@ data class User( @SerialName("phone_number") val phoneNumber: String, val name: String?, val role: String, - @SerialName("user_type") val userType: String? + @SerialName("user_type") val userType: String?, + @SerialName("created_at") val createdAt: String? = null, + @SerialName("country_code") val countryCode: String? = null ) @Serializable diff --git a/app/src/main/java/com/example/livingai_lg/ui/login_legacy/signup.md b/app/src/main/java/com/example/livingai_lg/ui/login_legacy/signup.md new file mode 100644 index 0000000..ba7f781 --- /dev/null +++ b/app/src/main/java/com/example/livingai_lg/ui/login_legacy/signup.md @@ -0,0 +1,192 @@ +# Signup API Endpoint + +## POST /auth/signup + +Creates a new user account with name, phone number, and optional location information. + +### Request Body + +```json +{ + "name": "John Doe", // Required: User's name (string, max 100 chars) + "phone_number": "+919876543210", // Required: Phone number in E.164 format + "state": "Maharashtra", // Optional: State name (string, max 100 chars) + "district": "Mumbai", // Optional: District name (string, max 100 chars) + "city_village": "Andheri", // Optional: City/Village name (string, max 150 chars) + "device_id": "device-123", // Optional: Device identifier + "device_info": { // Optional: Device information + "platform": "android", + "model": "Samsung Galaxy S21", + "os_version": "Android 13", + "app_version": "1.0.0", + "language_code": "en", + "timezone": "Asia/Kolkata" + } +} +``` + +### Success Response (201 Created) + +```json +{ + "success": true, + "user": { + "id": "uuid-here", + "phone_number": "+919876543210", + "name": "John Doe", + "country_code": "+91", + "created_at": "2024-01-15T10:30:00Z" + }, + "access_token": "jwt-access-token", + "refresh_token": "jwt-refresh-token", + "needs_profile": true, + "is_new_account": true, + "is_new_device": true, + "active_devices_count": 1, + "location_id": "uuid-of-location" // null if no location provided +} +``` + +### Error Responses + +#### 400 Bad Request - Validation Error +```json +{ + "error": "name is required" +} +``` + +#### 409 Conflict - User Already Exists +```json +{ + "success": false, + "message": "User with this phone number already exists. Please sign in instead.", + "user_exists": true +} +``` + +#### 403 Forbidden - IP Blocked +```json +{ + "success": false, + "message": "Access denied from this location." +} +``` + +#### 500 Internal Server Error +```json +{ + "success": false, + "message": "Internal server error" +} +``` + +### Features + +1. **User Existence Check**: Automatically checks if a user with the phone number already exists +2. **Phone Number Encryption**: Phone numbers are encrypted before storing in database +3. **Location Creation**: If state/district/city_village provided, creates a location entry +4. **Token Issuance**: Automatically issues access and refresh tokens +5. **Device Tracking**: Records device information for security +6. **Audit Logging**: Logs signup events for security monitoring + +### Example Usage + +#### cURL +```bash +curl -X POST http://localhost:3000/auth/signup \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "phone_number": "+919876543210", + "state": "Maharashtra", + "district": "Mumbai", + "city_village": "Andheri", + "device_id": "android-device-123", + "device_info": { + "platform": "android", + "model": "Samsung Galaxy S21", + "os_version": "Android 13" + } + }' +``` + +#### JavaScript/TypeScript +```javascript +const response = await fetch('/auth/signup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name: 'John Doe', + phone_number: '+919876543210', + state: 'Maharashtra', + district: 'Mumbai', + city_village: 'Andheri', + device_id: 'android-device-123', + device_info: { + platform: 'android', + model: 'Samsung Galaxy S21', + os_version: 'Android 13' + } + }) +}); + +const data = await response.json(); +if (data.success) { + // Store tokens + localStorage.setItem('access_token', data.access_token); + localStorage.setItem('refresh_token', data.refresh_token); +} +``` + +#### Kotlin/Android +```kotlin +data class SignupRequest( + val name: String, + val phone_number: String, + val state: String? = null, + val district: String? = null, + val city_village: String? = null, + val device_id: String? = null, + val device_info: Map? = null +) + +data class SignupResponse( + val success: Boolean, + val user: User, + val access_token: String, + val refresh_token: String, + val needs_profile: Boolean, + val is_new_account: Boolean, + val is_new_device: Boolean, + val active_devices_count: Int, + val location_id: String? +) + +// Usage +val request = SignupRequest( + name = "John Doe", + phone_number = "+919876543210", + state = "Maharashtra", + district = "Mumbai", + city_village = "Andheri", + device_id = getDeviceId(), + device_info = mapOf( + "platform" to "android", + "model" to Build.MODEL, + "os_version" to Build.VERSION.RELEASE + ) +) + +val response = apiClient.post("/auth/signup", request) +``` + +### Notes + +- Phone number must be in E.164 format (e.g., `+919876543210`) +- If phone number is 10 digits without `+`, it will be normalized to `+91` prefix +- Location fields are optional - user can be created without location +- If user already exists, returns 409 Conflict with `user_exists: true` +- All phone numbers are encrypted in the database for security +- Country code is automatically extracted from phone number + diff --git a/app/src/main/java/com/example/livingai_lg/ui/screens/auth/SignUpScreen.kt b/app/src/main/java/com/example/livingai_lg/ui/screens/auth/SignUpScreen.kt index ea4dd49..e1a129a 100644 --- a/app/src/main/java/com/example/livingai_lg/ui/screens/auth/SignUpScreen.kt +++ b/app/src/main/java/com/example/livingai_lg/ui/screens/auth/SignUpScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.livingai_lg.api.AuthApiClient import com.example.livingai_lg.api.AuthManager +import com.example.livingai_lg.api.SignupRequest import com.example.livingai_lg.api.TokenManager import com.example.livingai_lg.ui.components.backgrounds.DecorativeBackground import com.example.livingai_lg.ui.components.DropdownInput @@ -166,14 +167,25 @@ fun SignUpScreen( // Sign Up button Button( onClick = { - val fullPhoneNumber = "+91${formData.phoneNumber}" scope.launch { - authManager.requestOtp(fullPhoneNumber) + val fullPhoneNumber = "+91${formData.phoneNumber}" + val signupRequest = SignupRequest( + name = formData.name, + phoneNumber = fullPhoneNumber, + state = formData.state, + district = formData.district, + cityVillage = formData.village + ) + authManager.signup(signupRequest) .onSuccess { - onSignUpClick(fullPhoneNumber,formData.name) + if (it.success) { + onSignUpClick(fullPhoneNumber, formData.name) + } else { + Toast.makeText(context, it.message ?: "Signup failed", Toast.LENGTH_LONG).show() + } } .onFailure { - Toast.makeText(context, "Failed to send OTP: ${it.message}", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Signup failed: ${it.message}", Toast.LENGTH_LONG).show() } } },