Working Signup
This commit is contained in:
parent
6628daed50
commit
3c8288007a
|
|
@ -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<VerifyOtpResponse> = 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<SignupResponse> = 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<SignupResponse>()
|
||||
} else {
|
||||
response.body<SignupResponse>()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProfile(name: String, userType: String): Result<User> = 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SignupResponse> {
|
||||
return apiClient.signup(signupRequest)
|
||||
.onSuccess { response ->
|
||||
response.accessToken?.let { accessToken ->
|
||||
response.refreshToken?.let { refreshToken ->
|
||||
tokenManager.saveTokens(accessToken, refreshToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, String?>? = 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<SignupResponse>("/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
|
||||
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue