rewrite
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 33s

This commit is contained in:
2026-08-02 19:58:39 +05:00
parent 292bb29099
commit c6015e27a6
162 changed files with 18094 additions and 469 deletions
+182
View File
@@ -0,0 +1,182 @@
import { z } from "zod";
import { signUpFormSchema } from "@/lib/schemas";
import {
backendRegister,
checkIdOrPhone,
checkTempIdOrPhone,
} from "@/queries/authentication";
import apiClient from "@/lib/api-client";
import { tryCatch } from "@/utils/tryCatch";
const formSchema = z.object({
phoneNumber: z
.string()
.regex(/^[7|9][0-9]{2}-[0-9]{4}$/, "Please enter a valid phone number"),
});
export type FilterUserResponse = {
ok: boolean;
verified: boolean;
};
export type FilterTempUserResponse = {
ok: boolean;
otp_verified: boolean;
t_verified: boolean;
};
/**
* In the SPA port, actions can't `redirect()` server-side. Instead they return
* a `redirectTo` in their state and the form component navigates to it.
*/
export async function signin(_previousState: ActionState, formData: FormData) {
const phoneNumber = formData.get("phoneNumber") as string;
const result = formSchema.safeParse({ phoneNumber });
if (!result.success) {
return {
message: result.error.errors[0].message,
status: "error",
};
}
if (!phoneNumber) {
return {
message: "Please enter a phone number",
status: "error",
};
}
const FORMATTED_MOBILE_NUMBER = `${phoneNumber.split("-").join("")}`;
const user = await apiClient.get(
`/api/auth/users/filter/?mobile=${FORMATTED_MOBILE_NUMBER}`,
);
const userData = user.data as FilterUserResponse;
if (!userData.ok) {
return {
status: "redirect",
redirectTo: `/auth/signup?phone_number=${phoneNumber}`,
};
}
if (!userData.verified) {
return {
message:
"Your account is on pending verification. Please wait for a response from admin or contact shihaam.",
status: "error",
};
}
await apiClient.post("/auth/mobile/", { mobile: FORMATTED_MOBILE_NUMBER });
return {
status: "redirect",
redirectTo: `/auth/verify-otp?phone_number=${FORMATTED_MOBILE_NUMBER}`,
};
}
export type ActionState = {
status?: string;
redirectTo?: string;
message?: string;
payload?: FormData;
errors?: z.typeToFlattenedError<
{
id_card: string;
phone_number: string;
name: string;
atoll_id: string;
island_id: string;
address: string;
dob: Date;
terms: string;
policy: string;
accNo: string;
},
string
>;
db_error?: string;
error?: string;
};
export async function signup(_actionState: ActionState, formData: FormData) {
const data = Object.fromEntries(formData.entries());
const parsedData = signUpFormSchema.safeParse(data);
if (!parsedData.success) {
return {
message: "Invalid form data",
payload: formData,
errors: parsedData.error.flatten(),
};
}
const age =
new Date().getFullYear() - new Date(parsedData.data.dob).getFullYear();
if (age < 18) {
return {
message: "You must be at least 18 years old to register.",
payload: formData,
db_error: "dob",
};
}
const idCardExists = await checkIdOrPhone({
id_card: parsedData.data.id_card,
});
if (idCardExists.ok) {
return {
message: "ID card already exists.",
payload: formData,
db_error: "id_card",
};
}
const phoneNumberExists = await checkIdOrPhone({
phone_number: parsedData.data.phone_number,
});
const tempPhoneNumberExists = await checkTempIdOrPhone({
phone_number: parsedData.data.phone_number,
});
if (phoneNumberExists.ok || tempPhoneNumberExists.ok) {
return {
message: "Phone number already exists.",
payload: formData,
db_error: "phone_number",
};
}
const [signupError, signupResponse] = await tryCatch(
backendRegister({
payload: {
firstname: parsedData.data.name.split(" ")[0],
lastname: parsedData.data.name.split(" ").slice(1).join(" "),
username: parsedData.data.phone_number,
address: parsedData.data.address,
id_card: parsedData.data.id_card,
dob: new Date(parsedData.data.dob).toISOString().split("T")[0],
mobile: parsedData.data.phone_number,
island: Number.parseInt(parsedData.data.island_id),
atoll: Number.parseInt(parsedData.data.atoll_id),
acc_no: parsedData.data.accNo,
terms_accepted: parsedData.data.terms,
policy_accepted: parsedData.data.policy,
},
}),
);
if (signupError) {
return {
message: signupError.message,
payload: formData,
db_error: "phone_number",
};
}
return {
message: "User created successfully",
error: "success",
status: "redirect",
redirectTo: `/auth/verify-otp-registration?phone_number=${encodeURIComponent(
signupResponse.t_username,
)}`,
};
}
+199
View File
@@ -0,0 +1,199 @@
import type {
ApiError,
ApiResponse,
NewPayment,
Payment,
Topup,
} from "@/lib/backend-types";
import type { TopupResponse } from "@/lib/types";
import apiClient from "@/lib/api-client";
import { handleApiResponse } from "@/utils/tryCatch";
type GenericGetResponseProps = {
offset?: number;
limit?: number;
page?: number;
[key: string]: string | number | undefined;
};
function buildQuery(params: Record<string, string | number | undefined>) {
return Object.entries(params)
.filter(([_, value]) => value !== undefined && value !== "")
.map(
([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`,
)
.join("&");
}
export async function createPayment(data: NewPayment) {
const response = await apiClient.post("/api/billing/payment/", data);
return handleApiResponse<Payment>(response, "createPayment");
}
export async function createTopup(data: { amount: number }) {
const response = await apiClient.post("/api/billing/topup/", data);
return handleApiResponse<Topup>(response, "createTopup");
}
export async function getPayment({ id }: { id: string }) {
const response = await apiClient.get(`/api/billing/payment/${id}`);
return handleApiResponse<Payment>(response, "getPayment");
}
type GetPaymentProps = {
[key: string]: string | number | undefined;
};
export async function getPayments(params: GetPaymentProps, allPayments = false) {
const query = buildQuery(params);
const response = await apiClient.get(
`/api/billing/payment/?${query}&all_payments=${allPayments}`,
);
return handleApiResponse<ApiResponse<Payment>>(response, "getPayments");
}
export async function getTopups(
params: GenericGetResponseProps,
all_topups = false,
) {
const query = buildQuery(params);
const response = await apiClient.get(
`/api/billing/topup/?${query}&all_topups=${all_topups}`,
);
return handleApiResponse<ApiResponse<Topup>>(response, "getTopups");
}
export async function getTopup({ id }: { id: string }) {
const response = await apiClient.get(`/api/billing/topup/${id}`);
return handleApiResponse<Topup>(response, "getTopup");
}
export async function cancelTopup({ id }: { id: string }) {
const response = await apiClient.patch(`/api/billing/topup/${id}/cancel/`);
return handleApiResponse<Topup>(response, "cancelTopup");
}
export async function cancelPayment({ id }: { id: string }) {
const response = await apiClient.patch(`/api/billing/payment/${id}/cancel/`);
return handleApiResponse<Payment>(response, "cancelPayment");
}
type UpdatePayment = {
id: string;
method: "TRANSFER" | "WALLET";
};
export async function verifyPayment({ id, method }: UpdatePayment) {
const response = await apiClient.put(`/api/billing/payment/${id}/verify/`, {
method,
});
return handleApiResponse<Payment>(response, "verifyPayment");
}
export type VerifyDevicePaymentState = {
payment?: Payment;
message: string;
success: boolean;
fieldErrors: Record<string, string>;
payload?: FormData;
};
export async function verifyDevicePayment(
_prevState: VerifyDevicePaymentState,
formData: FormData,
): Promise<VerifyDevicePaymentState> {
const paymentId = formData.get("paymentId") as string;
const method = formData.get("method") as "TRANSFER" | "WALLET";
if (!paymentId) {
return {
message: "Payment ID is required",
success: false,
fieldErrors: { paymentId: "Payment ID is required" },
};
}
if (!method) {
return {
message: "Payment method is required",
success: false,
fieldErrors: { method: "Payment method is required" },
};
}
try {
const response = await apiClient.put(
`/api/billing/payment/${paymentId}/verify/`,
{ method },
);
const result = handleApiResponse<Payment>(response, "verifyDevicePayment");
return {
message:
method === "WALLET"
? "Payment completed successfully using wallet!"
: "Payment verification successful!",
success: true,
fieldErrors: {},
payment: result,
};
} catch (error: unknown) {
const fallback =
"Unable to verify payment. Please try again or contact support.";
return {
message: error instanceof Error ? error.message || fallback : fallback,
success: false,
fieldErrors: {},
};
}
}
export type VerifyTopupPaymentState = {
transaction?: {
sourceBank: string;
trxDate: string;
};
message: string;
success: boolean;
fieldErrors: Record<string, string>;
payload?: FormData;
};
export async function verifyTopupPayment(
_prevState: VerifyTopupPaymentState,
formData: FormData,
): Promise<VerifyTopupPaymentState> {
const topupId = formData.get("topupId") as string;
if (!topupId) {
return {
message: "Topup ID is required",
success: false,
fieldErrors: { topupId: "Topup ID is required" },
};
}
try {
const response = await apiClient.put(
`/api/billing/topup/${topupId}/verify/`,
);
const result = handleApiResponse<TopupResponse>(
response,
"verifyTopupPayment",
);
return {
message: result.message || "Topup payment verified successfully",
success: true,
fieldErrors: {},
transaction: result.transaction,
};
} catch (error: unknown) {
const fallback =
"Unable to verify payment. Please try again or contact support.";
return {
message: error instanceof Error ? error.message || fallback : fallback,
success: false,
fieldErrors: {},
};
}
}
+234
View File
@@ -0,0 +1,234 @@
import type { RejectUserFormState } from "@/components/user/user-reject-dialog";
import type { ApiError } from "@/lib/backend-types";
import type { User } from "@/lib/types/user";
import apiClient from "@/lib/api-client";
import { handleApiResponse } from "@/utils/tryCatch";
type VerifyUserResponse =
| {
ok: boolean;
mismatch_fields: string[] | null;
error: string | null;
detail: string | null;
}
| {
message: boolean;
};
export async function verifyUser(userId: string) {
try {
const r = await apiClient.put(`/api/auth/users/${userId}/verify/`);
const body = (r.data ?? {}) as VerifyUserResponse & {
message?: string;
detail?: string;
mismatch_fields?: string[] | null;
};
if (r.status < 200 || r.status >= 300) {
const msg = body?.message || body?.detail || "User verification failed";
return {
ok: false,
error: msg,
mismatch_fields: body?.mismatch_fields || null,
} as const;
}
return { ok: true, data: body } as const;
} catch (err) {
return { ok: false, error: (err as Error).message } as const;
}
}
export async function getProfile() {
const response = await apiClient.get("/api/auth/profile/");
return handleApiResponse<User>(response, "getProfile");
}
export async function rejectUser(
_prevState: RejectUserFormState,
formData: FormData,
): Promise<RejectUserFormState> {
const userId = formData.get("userId") as string;
const rejection_details = formData.get("rejection_details") as string;
const response = await apiClient.delete(
`/api/auth/users/${userId}/reject/`,
{ data: { rejection_details } },
);
if (response.status === 204) {
return {
message: "User rejected successfully!",
fieldErrors: {},
payload: formData,
redirectTo: "/users",
};
}
if (response.status < 200 || response.status >= 300) {
const errorData = (response.data ?? {}) as ApiError;
throw new Error(
errorData.message || errorData.detail || "Failed to reject user",
);
}
const error = (response.data ?? {}) as ApiError;
return {
message: error.message || error.detail || "An unexpected error occurred.",
fieldErrors: {},
payload: formData,
};
}
export type UpdateUserFormState = {
message: string;
fieldErrors?: {
id_card?: string[];
first_name?: string[];
last_name?: string[];
dob?: string[];
mobile?: string[];
address?: string[];
};
payload?: FormData;
};
export async function updateUser(
_prevState: UpdateUserFormState,
formData: FormData,
): Promise<UpdateUserFormState> {
const userId = formData.get("userId") as string;
const data: Record<string, string | number | boolean> = {};
for (const [key, value] of formData.entries()) {
if (key !== "userId" && value !== undefined && value !== "") {
data[key] = typeof value === "number" ? value : String(value);
}
}
const response = await apiClient.put(
`/api/auth/users/${userId}/update/`,
data,
);
const json = (response.data ?? {}) as Record<string, unknown> &
Partial<User> & { message?: string; detail?: string; field_errors?: unknown };
if (response.status < 200 || response.status >= 300) {
const isFieldErrorObject =
json &&
typeof json === "object" &&
!Array.isArray(json) &&
Object.values(json).every(
(val) => Array.isArray(val) && val.every((v) => typeof v === "string"),
);
return {
message:
json.message ||
json.detail ||
(isFieldErrorObject
? "Please correct the highlighted fields."
: "An error occurred while updating the user."),
fieldErrors: isFieldErrorObject
? (json as UpdateUserFormState["fieldErrors"])
: (json.field_errors as UpdateUserFormState["fieldErrors"]) || {},
payload: formData,
};
}
return {
...(json as unknown as User),
message: "User updated successfully",
};
}
export async function updateUserAgreement(
_prevState: UpdateUserFormState,
formData: FormData,
): Promise<UpdateUserFormState> {
const userId = formData.get("userId") as string;
// Remove userId from formData before sending to API
const apiFormData = new FormData();
for (const [key, value] of formData.entries()) {
if (key !== "userId") {
apiFormData.append(key, value);
}
}
// axios sets the multipart boundary content-type automatically for FormData.
const response = await apiClient.put(
`/api/auth/users/${userId}/agreement/`,
apiFormData,
);
if (response.status < 200 || response.status >= 300) {
const errorData = (response.data ?? {}) as ApiError & {
field_errors?: UpdateUserFormState["fieldErrors"];
};
return {
message:
errorData.message ||
errorData.detail ||
"An error occurred while updating the user agreement.",
fieldErrors: errorData.field_errors || {},
payload: formData,
};
}
const updatedUserAgreement = (response.data ?? {}) as { agreement: string };
return {
...updatedUserAgreement,
message: "User agreement updated successfully",
};
}
export type AddTopupFormState = {
status: boolean;
message: string;
fieldErrors?: {
amount?: string[];
description?: string[];
};
payload?: FormData;
};
export async function adminUserTopup(
_prevState: AddTopupFormState,
formData: FormData,
): Promise<AddTopupFormState> {
const user_id = formData.get("user_id") as string;
const amount = formData.get("amount") as string;
const description = formData.get("description") as string;
if (!amount) {
return {
status: false,
message: "Amount is required",
fieldErrors: { amount: ["Amount is required"], description: [] },
payload: formData,
};
}
const response = await apiClient.post("/api/billing/admin-topup/", {
amount: Number.parseInt(amount),
user_id: Number.parseInt(user_id),
description,
});
if (response.status < 200 || response.status >= 300) {
const errorData = (response.data ?? {}) as ApiError;
return {
status: false,
message:
errorData.message ||
errorData.detail ||
"An error occurred while topping up the user.",
fieldErrors: {},
payload: formData,
};
}
return {
status: true,
message: "User topped up successfully",
fieldErrors: {},
payload: formData,
};
}