template_0205
This commit is contained in:
73
RN_TEMPLATE/app/services/api/apiProblem.test.ts
Normal file
73
RN_TEMPLATE/app/services/api/apiProblem.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ApiErrorResponse } from "apisauce"
|
||||
|
||||
import { getGeneralApiProblem } from "./apiProblem"
|
||||
|
||||
test("handles connection errors", () => {
|
||||
expect(getGeneralApiProblem({ problem: "CONNECTION_ERROR" } as ApiErrorResponse<null>)).toEqual({
|
||||
kind: "cannot-connect",
|
||||
temporary: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("handles network errors", () => {
|
||||
expect(getGeneralApiProblem({ problem: "NETWORK_ERROR" } as ApiErrorResponse<null>)).toEqual({
|
||||
kind: "cannot-connect",
|
||||
temporary: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("handles timeouts", () => {
|
||||
expect(getGeneralApiProblem({ problem: "TIMEOUT_ERROR" } as ApiErrorResponse<null>)).toEqual({
|
||||
kind: "timeout",
|
||||
temporary: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("handles server errors", () => {
|
||||
expect(getGeneralApiProblem({ problem: "SERVER_ERROR" } as ApiErrorResponse<null>)).toEqual({
|
||||
kind: "server",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles unknown errors", () => {
|
||||
expect(getGeneralApiProblem({ problem: "UNKNOWN_ERROR" } as ApiErrorResponse<null>)).toEqual({
|
||||
kind: "unknown",
|
||||
temporary: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("handles unauthorized errors", () => {
|
||||
expect(
|
||||
getGeneralApiProblem({ problem: "CLIENT_ERROR", status: 401 } as ApiErrorResponse<null>),
|
||||
).toEqual({
|
||||
kind: "unauthorized",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles forbidden errors", () => {
|
||||
expect(
|
||||
getGeneralApiProblem({ problem: "CLIENT_ERROR", status: 403 } as ApiErrorResponse<null>),
|
||||
).toEqual({
|
||||
kind: "forbidden",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles not-found errors", () => {
|
||||
expect(
|
||||
getGeneralApiProblem({ problem: "CLIENT_ERROR", status: 404 } as ApiErrorResponse<null>),
|
||||
).toEqual({
|
||||
kind: "not-found",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles other client errors", () => {
|
||||
expect(
|
||||
getGeneralApiProblem({ problem: "CLIENT_ERROR", status: 418 } as ApiErrorResponse<null>),
|
||||
).toEqual({
|
||||
kind: "rejected",
|
||||
})
|
||||
})
|
||||
|
||||
test("handles cancellation errors", () => {
|
||||
expect(getGeneralApiProblem({ problem: "CANCEL_ERROR" } as ApiErrorResponse<null>)).toBeNull()
|
||||
})
|
||||
103
RN_TEMPLATE/app/services/api/apiProblem.ts
Normal file
103
RN_TEMPLATE/app/services/api/apiProblem.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { ApiResponse } from "apisauce"
|
||||
|
||||
// Base problem types without error details
|
||||
type BaseProblem =
|
||||
/**
|
||||
* Times up.
|
||||
*/
|
||||
| { kind: "timeout"; temporary: true }
|
||||
/**
|
||||
* Cannot connect to the server for some reason.
|
||||
*/
|
||||
| { kind: "cannot-connect"; temporary: true }
|
||||
/**
|
||||
* The server experienced a problem. Any 5xx error.
|
||||
*/
|
||||
| { kind: "server" }
|
||||
/**
|
||||
* We're not allowed because we haven't identified ourself. This is 401.
|
||||
*/
|
||||
| { kind: "unauthorized" }
|
||||
/**
|
||||
* We don't have access to perform that request. This is 403.
|
||||
*/
|
||||
| { kind: "forbidden" }
|
||||
/**
|
||||
* Unable to find that resource. This is a 404.
|
||||
*/
|
||||
| { kind: "not-found" }
|
||||
/**
|
||||
* All other 4xx series errors.
|
||||
*/
|
||||
| { kind: "rejected" }
|
||||
/**
|
||||
* Something truly unexpected happened. Most likely can try again. This is a catch all.
|
||||
*/
|
||||
| { kind: "unknown"; temporary: true }
|
||||
/**
|
||||
* The data we received is not in the expected format.
|
||||
*/
|
||||
| { kind: "bad-data" }
|
||||
|
||||
// Extended problem type that includes optional error code and message from API response
|
||||
export type GeneralApiProblem = BaseProblem & {
|
||||
errorCode?: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts error code from API response data.
|
||||
* Handles both { code: "E001" } and { error: { code: "E001" } } formats.
|
||||
*/
|
||||
function extractErrorCode(data: any): string | undefined {
|
||||
if (!data) return undefined
|
||||
return data.code || data.error?.code
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts error message from API response data.
|
||||
* Handles both { message: "..." } and { error: { message: "..." } } formats.
|
||||
*/
|
||||
function extractErrorMessage(data: any): string | undefined {
|
||||
if (!data) return undefined
|
||||
return data.message || data.error?.message
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to get a common cause of problems from an api response.
|
||||
*
|
||||
* @param response The api response.
|
||||
*/
|
||||
export function getGeneralApiProblem(response: ApiResponse<any>): GeneralApiProblem | null {
|
||||
// Extract error details from response data (if available)
|
||||
const errorCode = extractErrorCode(response.data)
|
||||
const errorMessage = extractErrorMessage(response.data)
|
||||
|
||||
switch (response.problem) {
|
||||
case "CONNECTION_ERROR":
|
||||
return { kind: "cannot-connect", temporary: true, errorCode, errorMessage }
|
||||
case "NETWORK_ERROR":
|
||||
return { kind: "cannot-connect", temporary: true, errorCode, errorMessage }
|
||||
case "TIMEOUT_ERROR":
|
||||
return { kind: "timeout", temporary: true, errorCode, errorMessage }
|
||||
case "SERVER_ERROR":
|
||||
return { kind: "server", errorCode, errorMessage }
|
||||
case "UNKNOWN_ERROR":
|
||||
return { kind: "unknown", temporary: true, errorCode, errorMessage }
|
||||
case "CLIENT_ERROR":
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
return { kind: "unauthorized", errorCode, errorMessage }
|
||||
case 403:
|
||||
return { kind: "forbidden", errorCode, errorMessage }
|
||||
case 404:
|
||||
return { kind: "not-found", errorCode, errorMessage }
|
||||
default:
|
||||
return { kind: "rejected", errorCode, errorMessage }
|
||||
}
|
||||
case "CANCEL_ERROR":
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
878
RN_TEMPLATE/app/services/api/authApi.ts
Normal file
878
RN_TEMPLATE/app/services/api/authApi.ts
Normal file
@@ -0,0 +1,878 @@
|
||||
/**
|
||||
* Authentication API Service
|
||||
*
|
||||
* Handles all authentication-related API calls:
|
||||
* - Pre-login (password verification + send verification code)
|
||||
* - Resend verification code
|
||||
* - Verify login (code verification + get tokens)
|
||||
* - Google login
|
||||
*/
|
||||
import { ApiResponse, ApisauceInstance, create } from "apisauce"
|
||||
|
||||
import Config from "@/config"
|
||||
|
||||
import { getGeneralApiProblem, GeneralApiProblem } from "./apiProblem"
|
||||
import type {
|
||||
BindNewEmailRequest,
|
||||
BindNewEmailResponse,
|
||||
ChangePasswordRequest,
|
||||
ChangePasswordResponse,
|
||||
ForgotPasswordRequest,
|
||||
ForgotPasswordResponse,
|
||||
GetLoginHistoryParams,
|
||||
GetLoginHistoryResponse,
|
||||
GetProfileResponse,
|
||||
GetSessionCountResponse,
|
||||
GetSessionsParams,
|
||||
GetSessionsResponse,
|
||||
GoogleLoginRequest,
|
||||
GoogleLoginResponse,
|
||||
LogoutResponse,
|
||||
PreLoginRequest,
|
||||
PreLoginResponse,
|
||||
PreRegisterRequest,
|
||||
PreRegisterResponse,
|
||||
RefreshTokenRequest,
|
||||
RefreshTokenResponse,
|
||||
ResendCodeRequest,
|
||||
ResendCodeResponse,
|
||||
ResendForgotPasswordRequest,
|
||||
ResendForgotPasswordResponse,
|
||||
ResendRegisterCodeRequest,
|
||||
ResendRegisterCodeResponse,
|
||||
ResetPasswordRequest,
|
||||
ResetPasswordResponse,
|
||||
RevokeOtherSessionsResponse,
|
||||
RevokeSessionResponse,
|
||||
SendEmailCodeRequest,
|
||||
SendEmailCodeResponse,
|
||||
UpdateProfileRequest,
|
||||
UpdateProfileResponse,
|
||||
VerifyCurrentEmailRequest,
|
||||
VerifyCurrentEmailResponse,
|
||||
VerifyLoginRequest,
|
||||
VerifyLoginResponse,
|
||||
VerifyRegisterRequest,
|
||||
VerifyRegisterResponse,
|
||||
} from "./authTypes"
|
||||
|
||||
const AUTH_API_CONFIG = {
|
||||
url: Config.AUTH_API_URL || "https://auth.upay01.com",
|
||||
timeout: 15000,
|
||||
}
|
||||
|
||||
/**
|
||||
* Auth API class for handling authentication requests
|
||||
*/
|
||||
export class AuthApi {
|
||||
apisauce: ApisauceInstance
|
||||
|
||||
constructor() {
|
||||
this.apisauce = create({
|
||||
baseURL: AUTH_API_CONFIG.url,
|
||||
timeout: AUTH_API_CONFIG.timeout,
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-login: Verify password and send verification code
|
||||
*/
|
||||
async preLogin(
|
||||
email: string,
|
||||
password: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: PreLoginResponse } | GeneralApiProblem> {
|
||||
const payload: PreLoginRequest = { email, password }
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<PreLoginResponse> = await this.apisauce.post(
|
||||
"/api/auth/pre-login",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth preLogin error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend verification code
|
||||
*/
|
||||
async resendCode(
|
||||
email: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: ResendCodeResponse } | GeneralApiProblem> {
|
||||
const payload: ResendCodeRequest = { email }
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<ResendCodeResponse> = await this.apisauce.post(
|
||||
"/api/auth/pre-login/resend",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth resendCode error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify login: Verify code and get tokens
|
||||
*/
|
||||
async verifyLogin(
|
||||
email: string,
|
||||
code: string,
|
||||
): Promise<{ kind: "ok"; data: VerifyLoginResponse } | GeneralApiProblem> {
|
||||
const payload: VerifyLoginRequest = { email, code }
|
||||
|
||||
const response: ApiResponse<VerifyLoginResponse> = await this.apisauce.post(
|
||||
"/api/auth/login/verify",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth verifyLogin error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google login
|
||||
*/
|
||||
async googleLogin(
|
||||
idToken: string,
|
||||
referralCode?: string,
|
||||
): Promise<{ kind: "ok"; data: GoogleLoginResponse } | GeneralApiProblem> {
|
||||
const payload: GoogleLoginRequest = { idToken }
|
||||
if (referralCode) payload.referralCode = referralCode
|
||||
|
||||
const response: ApiResponse<GoogleLoginResponse> = await this.apisauce.post(
|
||||
"/api/auth/google-login",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth googleLogin error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-register: Validate data and send verification code
|
||||
*/
|
||||
async preRegister(
|
||||
username: string,
|
||||
password: string,
|
||||
email: string,
|
||||
nickname?: string,
|
||||
referralCode?: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: PreRegisterResponse } | GeneralApiProblem> {
|
||||
const payload: PreRegisterRequest = { username, password, email }
|
||||
if (nickname) payload.nickname = nickname
|
||||
if (referralCode) payload.referralCode = referralCode
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<PreRegisterResponse> = await this.apisauce.post(
|
||||
"/api/auth/pre-register",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth preRegister error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend register verification code
|
||||
*/
|
||||
async resendRegisterCode(
|
||||
email: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: ResendRegisterCodeResponse } | GeneralApiProblem> {
|
||||
const payload: ResendRegisterCodeRequest = { email }
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<ResendRegisterCodeResponse> = await this.apisauce.post(
|
||||
"/api/auth/pre-register/resend",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth resendRegisterCode error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify register: Verify code and create user
|
||||
*/
|
||||
async verifyRegister(
|
||||
email: string,
|
||||
code: string,
|
||||
): Promise<{ kind: "ok"; data: VerifyRegisterResponse } | GeneralApiProblem> {
|
||||
const payload: VerifyRegisterRequest = { email, code }
|
||||
|
||||
const response: ApiResponse<VerifyRegisterResponse> = await this.apisauce.post(
|
||||
"/api/auth/register/verify",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth verifyRegister error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot password: Send verification code
|
||||
*/
|
||||
async forgotPassword(
|
||||
email: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: ForgotPasswordResponse } | GeneralApiProblem> {
|
||||
const payload: ForgotPasswordRequest = { email }
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<ForgotPasswordResponse> = await this.apisauce.post(
|
||||
"/api/auth/forgot-password",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth forgotPassword error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend forgot password verification code
|
||||
*/
|
||||
async resendForgotPasswordCode(
|
||||
email: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: ResendForgotPasswordResponse } | GeneralApiProblem> {
|
||||
const payload: ResendForgotPasswordRequest = { email }
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<ResendForgotPasswordResponse> = await this.apisauce.post(
|
||||
"/api/auth/forgot-password/resend",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth resendForgotPasswordCode error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password: Verify code and set new password
|
||||
*/
|
||||
async resetPassword(
|
||||
email: string,
|
||||
code: string,
|
||||
newPassword: string,
|
||||
): Promise<{ kind: "ok"; data: ResetPasswordResponse } | GeneralApiProblem> {
|
||||
const payload: ResetPasswordRequest = { email, code, newPassword }
|
||||
|
||||
const response: ApiResponse<ResetPasswordResponse> = await this.apisauce.post(
|
||||
"/api/auth/reset-password",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth resetPassword error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout: Revoke token
|
||||
*/
|
||||
async logoutApi(
|
||||
accessToken: string,
|
||||
): Promise<{ kind: "ok"; data: LogoutResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<LogoutResponse> = await this.apisauce.post(
|
||||
"/api/auth/logout",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth logout error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Profile APIs
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get user profile
|
||||
*/
|
||||
async getProfile(
|
||||
accessToken: string,
|
||||
): Promise<{ kind: "ok"; data: GetProfileResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<GetProfileResponse> = await this.apisauce.get(
|
||||
"/api/auth/profile",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth getProfile error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile
|
||||
*/
|
||||
async updateProfile(
|
||||
accessToken: string,
|
||||
payload: UpdateProfileRequest,
|
||||
): Promise<{ kind: "ok"; data: UpdateProfileResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<UpdateProfileResponse> = await this.apisauce.put(
|
||||
"/api/auth/profile",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth updateProfile error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Password APIs
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Change password
|
||||
*/
|
||||
async changePassword(
|
||||
accessToken: string,
|
||||
payload: ChangePasswordRequest,
|
||||
): Promise<{ kind: "ok"; data: ChangePasswordResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<ChangePasswordResponse> = await this.apisauce.post(
|
||||
"/api/auth/change-password",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth changePassword error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Token APIs
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
async refreshToken(
|
||||
refreshToken: string,
|
||||
): Promise<{ kind: "ok"; data: RefreshTokenResponse } | GeneralApiProblem> {
|
||||
const payload: RefreshTokenRequest = { refreshToken }
|
||||
|
||||
const response: ApiResponse<RefreshTokenResponse> = await this.apisauce.post(
|
||||
"/api/auth/refresh-token",
|
||||
payload,
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth refreshToken error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Email APIs
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Send email verification code (for changing email)
|
||||
*/
|
||||
async sendEmailCode(
|
||||
accessToken: string,
|
||||
email: string,
|
||||
language?: string,
|
||||
): Promise<{ kind: "ok"; data: SendEmailCodeResponse } | GeneralApiProblem> {
|
||||
const payload: SendEmailCodeRequest = { email }
|
||||
if (language) payload.language = language
|
||||
|
||||
const response: ApiResponse<SendEmailCodeResponse> = await this.apisauce.post(
|
||||
"/api/email/send-verification-code",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth sendEmailCode error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify current email (Step 1 of email change)
|
||||
*/
|
||||
async verifyCurrentEmail(
|
||||
accessToken: string,
|
||||
currentCode: string,
|
||||
): Promise<{ kind: "ok"; data: VerifyCurrentEmailResponse } | GeneralApiProblem> {
|
||||
const payload: VerifyCurrentEmailRequest = {
|
||||
action: "verify-current",
|
||||
currentCode,
|
||||
}
|
||||
|
||||
const response: ApiResponse<VerifyCurrentEmailResponse> = await this.apisauce.post(
|
||||
"/api/email/change",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth verifyCurrentEmail error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind new email (Step 2 of email change)
|
||||
*/
|
||||
async bindNewEmail(
|
||||
accessToken: string,
|
||||
newEmail: string,
|
||||
newCode: string,
|
||||
): Promise<{ kind: "ok"; data: BindNewEmailResponse } | GeneralApiProblem> {
|
||||
const payload: BindNewEmailRequest = {
|
||||
action: "bind-new",
|
||||
newEmail,
|
||||
newCode,
|
||||
}
|
||||
|
||||
const response: ApiResponse<BindNewEmailResponse> = await this.apisauce.post(
|
||||
"/api/email/change",
|
||||
payload,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth bindNewEmail error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Session Management APIs
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Get sessions/devices list
|
||||
*/
|
||||
async getSessions(
|
||||
accessToken: string,
|
||||
params?: GetSessionsParams,
|
||||
): Promise<{ kind: "ok"; data: GetSessionsResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<GetSessionsResponse> = await this.apisauce.get(
|
||||
"/api/sessions",
|
||||
params,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth getSessions error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active session count
|
||||
*/
|
||||
async getSessionCount(
|
||||
accessToken: string,
|
||||
): Promise<{ kind: "ok"; data: GetSessionCountResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<GetSessionCountResponse> = await this.apisauce.get(
|
||||
"/api/sessions/count",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth getSessionCount error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get login history
|
||||
*/
|
||||
async getLoginHistory(
|
||||
accessToken: string,
|
||||
params?: GetLoginHistoryParams,
|
||||
): Promise<{ kind: "ok"; data: GetLoginHistoryResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<GetLoginHistoryResponse> = await this.apisauce.get(
|
||||
"/api/sessions/history",
|
||||
params,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth getLoginHistory error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a specific session (remote logout device)
|
||||
*/
|
||||
async revokeSession(
|
||||
accessToken: string,
|
||||
sessionId: string,
|
||||
): Promise<{ kind: "ok"; data: RevokeSessionResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<RevokeSessionResponse> = await this.apisauce.delete(
|
||||
`/api/sessions/${sessionId}`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth revokeSession error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke all other sessions (logout all other devices)
|
||||
*/
|
||||
async revokeOtherSessions(
|
||||
accessToken: string,
|
||||
): Promise<{ kind: "ok"; data: RevokeOtherSessionsResponse } | GeneralApiProblem> {
|
||||
const response: ApiResponse<RevokeOtherSessionsResponse> = await this.apisauce.post(
|
||||
"/api/sessions/revoke-others",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
try {
|
||||
const data = response.data
|
||||
if (!data) return { kind: "bad-data" }
|
||||
return { kind: "ok", data }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Auth revokeOtherSessions error: ${e.message}`)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const authApi = new AuthApi()
|
||||
421
RN_TEMPLATE/app/services/api/authTypes.ts
Normal file
421
RN_TEMPLATE/app/services/api/authTypes.ts
Normal file
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* Authentication API Types
|
||||
*/
|
||||
|
||||
// Base API response with optional error code
|
||||
export interface BaseApiResponse {
|
||||
success: boolean
|
||||
message?: string
|
||||
code?: string // Error code at root level like E001, E011, etc.
|
||||
error?: {
|
||||
code?: string // Error code in nested error object
|
||||
message?: string
|
||||
}
|
||||
}
|
||||
|
||||
// User profile
|
||||
export interface UserProfile {
|
||||
nickname: string
|
||||
avatar: string
|
||||
}
|
||||
|
||||
// User information
|
||||
export interface User {
|
||||
_id: string
|
||||
userId: number
|
||||
username: string
|
||||
email: string
|
||||
emailVerified: boolean
|
||||
loginMethods: string[]
|
||||
roles: string[]
|
||||
profile: UserProfile
|
||||
status: string
|
||||
referralCode: string
|
||||
referredBy?: string
|
||||
referredByCode?: string
|
||||
lastLogin: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Tokens
|
||||
export interface Tokens {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
accessTokenExpiry: string
|
||||
refreshTokenExpiry: string
|
||||
}
|
||||
|
||||
// Pre-login request
|
||||
export interface PreLoginRequest {
|
||||
email: string
|
||||
password: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Pre-login response
|
||||
export interface PreLoginResponse extends BaseApiResponse {}
|
||||
|
||||
// Resend code request
|
||||
export interface ResendCodeRequest {
|
||||
email: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Resend code response
|
||||
export interface ResendCodeResponse extends BaseApiResponse {}
|
||||
|
||||
// Verify login request
|
||||
export interface VerifyLoginRequest {
|
||||
email: string
|
||||
code: string
|
||||
}
|
||||
|
||||
// Verify login response
|
||||
export interface VerifyLoginResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
user: User
|
||||
tokens: Tokens
|
||||
}
|
||||
}
|
||||
|
||||
// Google login request
|
||||
export interface GoogleLoginRequest {
|
||||
idToken: string
|
||||
referralCode?: string
|
||||
}
|
||||
|
||||
// Google login response
|
||||
export interface GoogleLoginResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
user: User
|
||||
tokens: Tokens
|
||||
}
|
||||
}
|
||||
|
||||
// Login step type
|
||||
export type LoginStep = "credentials" | "verification"
|
||||
|
||||
// Register step type
|
||||
export type RegisterStep = "credentials" | "verification"
|
||||
|
||||
// Pre-register request
|
||||
export interface PreRegisterRequest {
|
||||
username: string
|
||||
password: string
|
||||
email: string
|
||||
nickname?: string
|
||||
referralCode?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Pre-register response
|
||||
export interface PreRegisterResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
email: string
|
||||
expiresIn: string
|
||||
}
|
||||
}
|
||||
|
||||
// Resend register code request
|
||||
export interface ResendRegisterCodeRequest {
|
||||
email: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Resend register code response
|
||||
export interface ResendRegisterCodeResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
email: string
|
||||
expiresIn: string
|
||||
}
|
||||
}
|
||||
|
||||
// Verify register request
|
||||
export interface VerifyRegisterRequest {
|
||||
email: string
|
||||
code: string
|
||||
}
|
||||
|
||||
// Verify register response
|
||||
export interface VerifyRegisterResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
user: User
|
||||
tokens: Tokens
|
||||
}
|
||||
}
|
||||
|
||||
// Forgot password step type
|
||||
export type ForgotPasswordStep = "email" | "reset"
|
||||
|
||||
// Forgot password request
|
||||
export interface ForgotPasswordRequest {
|
||||
email: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Forgot password response
|
||||
export interface ForgotPasswordResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
email: string
|
||||
expiresIn: number
|
||||
}
|
||||
}
|
||||
|
||||
// Resend forgot password code request
|
||||
export interface ResendForgotPasswordRequest {
|
||||
email: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Resend forgot password code response
|
||||
export interface ResendForgotPasswordResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
email: string
|
||||
expiresIn: number
|
||||
}
|
||||
}
|
||||
|
||||
// Reset password request
|
||||
export interface ResetPasswordRequest {
|
||||
email: string
|
||||
code: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
// Reset password response
|
||||
export interface ResetPasswordResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
// Logout response
|
||||
export interface LogoutResponse extends BaseApiResponse {}
|
||||
|
||||
// ============================================
|
||||
// Profile API Types
|
||||
// ============================================
|
||||
|
||||
// Get profile response
|
||||
export interface GetProfileResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
userID: number
|
||||
username: string
|
||||
email: string
|
||||
profile: UserProfile
|
||||
roles: string[]
|
||||
referralCode: string
|
||||
hasReferrer: boolean
|
||||
referredByCode?: string
|
||||
status: string
|
||||
lastLogin: string
|
||||
createdAt: string
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile request
|
||||
export interface UpdateProfileRequest {
|
||||
nickname?: string
|
||||
avatar?: string
|
||||
}
|
||||
|
||||
// Update profile response
|
||||
export interface UpdateProfileResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
user: User
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Change Password API Types
|
||||
// ============================================
|
||||
|
||||
// Change password request
|
||||
export interface ChangePasswordRequest {
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
logoutOtherDevices?: boolean
|
||||
}
|
||||
|
||||
// Change password response
|
||||
export interface ChangePasswordResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
loggedOutOtherDevices: boolean
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Refresh Token API Types
|
||||
// ============================================
|
||||
|
||||
// Refresh token request
|
||||
export interface RefreshTokenRequest {
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
// Refresh token response
|
||||
export interface RefreshTokenResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
accessToken: string
|
||||
accessTokenExpiry: string
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Email Change API Types
|
||||
// ============================================
|
||||
|
||||
// Send email verification code request
|
||||
export interface SendEmailCodeRequest {
|
||||
email: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
// Send email verification code response
|
||||
export interface SendEmailCodeResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
email: string
|
||||
expiresIn: string
|
||||
}
|
||||
}
|
||||
|
||||
// Verify current email request (Step 1)
|
||||
export interface VerifyCurrentEmailRequest {
|
||||
action: "verify-current"
|
||||
currentCode: string
|
||||
}
|
||||
|
||||
// Verify current email response
|
||||
export interface VerifyCurrentEmailResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
currentEmailVerified: boolean
|
||||
expiresIn: string
|
||||
}
|
||||
}
|
||||
|
||||
// Bind new email request (Step 2)
|
||||
export interface BindNewEmailRequest {
|
||||
action: "bind-new"
|
||||
newEmail: string
|
||||
newCode: string
|
||||
}
|
||||
|
||||
// Bind new email response
|
||||
export interface BindNewEmailResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
user: User
|
||||
}
|
||||
}
|
||||
|
||||
// Union type for email change requests
|
||||
export type EmailChangeRequest = VerifyCurrentEmailRequest | BindNewEmailRequest
|
||||
|
||||
// Union type for email change responses
|
||||
export type EmailChangeResponse = VerifyCurrentEmailResponse | BindNewEmailResponse
|
||||
|
||||
// ============================================
|
||||
// Session Management API Types
|
||||
// ============================================
|
||||
|
||||
// Device info for session
|
||||
export interface DeviceInfo {
|
||||
deviceId?: string
|
||||
deviceName?: string
|
||||
deviceType: string
|
||||
os: string
|
||||
osVersion?: string
|
||||
browser: string
|
||||
browserVersion?: string
|
||||
userAgent?: string
|
||||
}
|
||||
|
||||
// Location info for session
|
||||
export interface LocationInfo {
|
||||
country?: string
|
||||
city?: string
|
||||
}
|
||||
|
||||
// Session item
|
||||
export interface Session {
|
||||
id: string
|
||||
userId: string
|
||||
tokenJti: string
|
||||
deviceInfo: DeviceInfo
|
||||
ipAddress: string
|
||||
location?: LocationInfo
|
||||
loginMethod: string
|
||||
isActive: boolean
|
||||
isCurrent: boolean
|
||||
lastActiveAt: string
|
||||
createdAt: string
|
||||
expiresAt: string
|
||||
}
|
||||
|
||||
// Get sessions request params
|
||||
export interface GetSessionsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
// Get sessions response
|
||||
export interface GetSessionsResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
sessions: Session[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
// Get session count response
|
||||
export interface GetSessionCountResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
activeCount: number
|
||||
}
|
||||
}
|
||||
|
||||
// Login history record
|
||||
export interface LoginHistoryRecord {
|
||||
id: string
|
||||
userId: string
|
||||
sessionId?: string
|
||||
deviceInfo: DeviceInfo
|
||||
ipAddress: string
|
||||
location?: LocationInfo
|
||||
loginMethod: string
|
||||
status: "success" | "failed"
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
// Get login history params
|
||||
export interface GetLoginHistoryParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
status?: "success" | "failed"
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
// Get login history response
|
||||
export interface GetLoginHistoryResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
records: LoginHistoryRecord[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke session response
|
||||
export interface RevokeSessionResponse extends BaseApiResponse {}
|
||||
|
||||
// Revoke other sessions response
|
||||
export interface RevokeOtherSessionsResponse extends BaseApiResponse {
|
||||
data?: {
|
||||
revokedCount: number
|
||||
}
|
||||
}
|
||||
82
RN_TEMPLATE/app/services/api/index.ts
Normal file
82
RN_TEMPLATE/app/services/api/index.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* This Api class lets you define an API endpoint and methods to request
|
||||
* data and process it.
|
||||
*
|
||||
* See the [Backend API Integration](https://docs.infinite.red/ignite-cli/boilerplate/app/services/#backend-api-integration)
|
||||
* documentation for more details.
|
||||
*/
|
||||
import { ApiResponse, ApisauceInstance, create } from "apisauce"
|
||||
|
||||
import Config from "@/config"
|
||||
import type { EpisodeItem } from "@/services/api/types"
|
||||
|
||||
import { GeneralApiProblem, getGeneralApiProblem } from "./apiProblem"
|
||||
import type { ApiConfig, ApiFeedResponse } from "./types"
|
||||
|
||||
/**
|
||||
* Configuring the apisauce instance.
|
||||
*/
|
||||
export const DEFAULT_API_CONFIG: ApiConfig = {
|
||||
url: Config.API_URL,
|
||||
timeout: 10000,
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages all requests to the API. You can use this class to build out
|
||||
* various requests that you need to call from your backend API.
|
||||
*/
|
||||
export class Api {
|
||||
apisauce: ApisauceInstance
|
||||
config: ApiConfig
|
||||
|
||||
/**
|
||||
* Set up our API instance. Keep this lightweight!
|
||||
*/
|
||||
constructor(config: ApiConfig = DEFAULT_API_CONFIG) {
|
||||
this.config = config
|
||||
this.apisauce = create({
|
||||
baseURL: this.config.url,
|
||||
timeout: this.config.timeout,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of recent React Native Radio episodes.
|
||||
*/
|
||||
async getEpisodes(): Promise<{ kind: "ok"; episodes: EpisodeItem[] } | GeneralApiProblem> {
|
||||
// make the api call
|
||||
const response: ApiResponse<ApiFeedResponse> = await this.apisauce.get(
|
||||
`api.json?rss_url=https%3A%2F%2Ffeeds.simplecast.com%2FhEI_f9Dx`,
|
||||
)
|
||||
|
||||
// the typical ways to die when calling an api
|
||||
if (!response.ok) {
|
||||
const problem = getGeneralApiProblem(response)
|
||||
if (problem) return problem
|
||||
}
|
||||
|
||||
// transform the data into the format we are expecting
|
||||
try {
|
||||
const rawData = response.data
|
||||
|
||||
// This is where we transform the data into the shape we expect for our model.
|
||||
const episodes: EpisodeItem[] =
|
||||
rawData?.items.map((raw) => ({
|
||||
...raw,
|
||||
})) ?? []
|
||||
|
||||
return { kind: "ok", episodes }
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Bad data: ${e.message}\n${response.data}`, e.stack)
|
||||
}
|
||||
return { kind: "bad-data" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance of the API for convenience
|
||||
export const api = new Api()
|
||||
50
RN_TEMPLATE/app/services/api/types.ts
Normal file
50
RN_TEMPLATE/app/services/api/types.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* These types indicate the shape of the data you expect to receive from your
|
||||
* API endpoint, assuming it's a JSON object like we have.
|
||||
*/
|
||||
export interface EpisodeItem {
|
||||
title: string
|
||||
pubDate: string
|
||||
link: string
|
||||
guid: string
|
||||
author: string
|
||||
thumbnail: string
|
||||
description: string
|
||||
content: string
|
||||
enclosure: {
|
||||
link: string
|
||||
type: string
|
||||
length: number
|
||||
duration: number
|
||||
rating: { scheme: string; value: string }
|
||||
}
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export interface ApiFeedResponse {
|
||||
status: string
|
||||
feed: {
|
||||
url: string
|
||||
title: string
|
||||
link: string
|
||||
author: string
|
||||
description: string
|
||||
image: string
|
||||
}
|
||||
items: EpisodeItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The options used to configure apisauce.
|
||||
*/
|
||||
export interface ApiConfig {
|
||||
/**
|
||||
* The URL of the api.
|
||||
*/
|
||||
url: string
|
||||
|
||||
/**
|
||||
* Milliseconds before we timeout the request.
|
||||
*/
|
||||
timeout: number
|
||||
}
|
||||
150
RN_TEMPLATE/app/services/api/uploadApi.ts
Normal file
150
RN_TEMPLATE/app/services/api/uploadApi.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Upload API for file uploads via proxy to MinIO
|
||||
* Supports both Web and Native platforms
|
||||
*/
|
||||
import { Platform } from "react-native"
|
||||
|
||||
const OSS_BASE_URL = "https://oss.upay01.com"
|
||||
|
||||
// Upload response type
|
||||
export interface UploadResponse {
|
||||
code: number
|
||||
message: string
|
||||
data?: {
|
||||
filename: string
|
||||
originalName: string
|
||||
size: number
|
||||
mimetype: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a URI to Blob (for Web platform)
|
||||
*/
|
||||
async function uriToBlob(uri: string): Promise<Blob> {
|
||||
const response = await fetch(uri)
|
||||
const blob = await response.blob()
|
||||
return blob
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file on Web platform
|
||||
*/
|
||||
async function uploadFileWeb(
|
||||
uri: string,
|
||||
fileName: string,
|
||||
mimeType: string,
|
||||
): Promise<{ kind: "ok"; url: string } | { kind: "error"; message: string }> {
|
||||
try {
|
||||
// Convert URI to Blob for Web
|
||||
const blob = await uriToBlob(uri)
|
||||
const file = new File([blob], fileName, { type: mimeType })
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
|
||||
const response = await fetch(`${OSS_BASE_URL}/api/proxy/upload`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const data: UploadResponse = await response.json()
|
||||
|
||||
if (response.ok && data.code === 200 && data.data?.url) {
|
||||
return { kind: "ok", url: data.data.url }
|
||||
} else {
|
||||
return { kind: "error", message: data.message || "Upload failed" }
|
||||
}
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Upload error: ${e.message}`)
|
||||
}
|
||||
return { kind: "error", message: "Upload failed" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file on Native platform (iOS/Android)
|
||||
*/
|
||||
function uploadFileNative(
|
||||
uri: string,
|
||||
fileName: string,
|
||||
mimeType: string,
|
||||
): Promise<{ kind: "ok"; url: string } | { kind: "error"; message: string }> {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const xhr = new XMLHttpRequest()
|
||||
|
||||
xhr.onload = () => {
|
||||
if (__DEV__) {
|
||||
console.log("Upload response status:", xhr.status)
|
||||
console.log("Upload response body:", xhr.responseText)
|
||||
}
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response: UploadResponse = JSON.parse(xhr.responseText)
|
||||
if (response.code === 200 && response.data?.url) {
|
||||
resolve({ kind: "ok", url: response.data.url })
|
||||
} else {
|
||||
resolve({ kind: "error", message: response.message || "Upload failed" })
|
||||
}
|
||||
} catch {
|
||||
resolve({ kind: "error", message: "Invalid response" })
|
||||
}
|
||||
} else {
|
||||
if (__DEV__) {
|
||||
console.log("Upload failed - Status:", xhr.status, "Response:", xhr.responseText)
|
||||
}
|
||||
resolve({ kind: "error", message: `Upload failed with status ${xhr.status}` })
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onerror = () => {
|
||||
resolve({ kind: "error", message: "Network error" })
|
||||
}
|
||||
|
||||
xhr.ontimeout = () => {
|
||||
resolve({ kind: "error", message: "Upload timeout" })
|
||||
}
|
||||
|
||||
xhr.open("POST", `${OSS_BASE_URL}/api/proxy/upload`)
|
||||
xhr.timeout = 60000
|
||||
|
||||
// React Native specific FormData format
|
||||
const formData = new FormData()
|
||||
formData.append("file", {
|
||||
uri: uri,
|
||||
type: mimeType,
|
||||
name: fileName,
|
||||
} as any)
|
||||
|
||||
xhr.send(formData)
|
||||
} catch (e) {
|
||||
if (__DEV__ && e instanceof Error) {
|
||||
console.error(`Upload error: ${e.message}`)
|
||||
}
|
||||
resolve({ kind: "error", message: "Upload failed" })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to OSS storage
|
||||
* Automatically handles Web vs Native platform differences
|
||||
* @param uri - Local file URI
|
||||
* @param fileName - File name
|
||||
* @param mimeType - MIME type of the file
|
||||
* @returns Upload result with URL or error
|
||||
*/
|
||||
export async function uploadFile(
|
||||
uri: string,
|
||||
fileName: string,
|
||||
mimeType: string,
|
||||
): Promise<{ kind: "ok"; url: string } | { kind: "error"; message: string }> {
|
||||
if (Platform.OS === "web") {
|
||||
return uploadFileWeb(uri, fileName, mimeType)
|
||||
} else {
|
||||
return uploadFileNative(uri, fileName, mimeType)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user