diff --git a/Filestore Frontend Guide.md b/Filestore Frontend Guide.md new file mode 100644 index 0000000..6568093 --- /dev/null +++ b/Filestore Frontend Guide.md @@ -0,0 +1,276 @@ +# Frontend Guide: File Upload + Profile Image APIs + +This is a simple reference for frontend integration with file upload, file retrieval, and setting profile image. + +## Base URLs + +- Admin APIs: `/admin/v1` +- Customer/Public APIs: `/api/v1` + +All protected endpoints require: + +- `Authorization: Bearer ` + +--- + +## 1) Login (Admin) + +Use this when you want to update admin user profile image. + +### Request + +- `POST /admin/v1/profile/login` +- `Content-Type: application/json` + +```json +{ + "username": "admin", + "password": "Admin1234" +} +``` + +### Response (success) + +```json +{ + "success": true, + "message": "Login successful", + "data": { + "user": { + "id": "65bc1234567890abcdef1234", + "profile_image": null + }, + "access_token": "", + "refresh_token": "", + "expires_at": 1776783486 + } +} +``` + +--- + +## 2) Upload File (GridFS) + +Use multipart form upload. + +### Admin route + +- `POST /admin/v1/files/upload` + +### Customer route + +- `POST /api/v1/files/upload` + +### Form fields + +- `file` (required, File) +- `category` (optional, string), example: `profile_images` +- `tags` (optional, repeatable), example: `user`, `profile` +- `description` (optional, string) + +### Response (success) + +```json +{ + "file_id": "69e7899c2053abd3fa5433e0", + "filename": "avatar.png", + "content_type": "image/png", + "size": 123456, + "message": "File uploaded successfully" +} +``` + +--- + +## 3) Get File Info + +- `GET /admin/v1/files/{file_id}/info` +- `GET /api/v1/files/{file_id}/info` + +### Response (success) + +```json +{ + "file_id": "69e7899c2053abd3fa5433e0", + "filename": "avatar.png", + "content_type": "image/png", + "size": 123456, + "uploaded_by": "65bc1234567890abcdef1234", + "uploaded_at": "2026-04-21T14:28:44.787Z", + "category": "profile_images", + "tags": ["user", "profile"], + "description": "sample profile photo" +} +``` + +--- + +## 4) Download File + +- `GET /admin/v1/files/{file_id}/download` +- `GET /api/v1/files/{file_id}/download` + +Returns file binary stream. + +Useful for image URL: + +- `http://localhost:8080/admin/v1/files//download` +- or `http://localhost:8080/api/v1/files//download` + +--- + +## 5) Set Admin User Profile Image + +Upload does not update user profile automatically. +After upload, call profile update endpoint with the download URL. + +### Request + +- `PUT /admin/v1/profile` +- `Content-Type: application/json` + +```json +{ + "profile_image": "http://localhost:8080/admin/v1/files/69e7899c2053abd3fa5433e0/download" +} +``` + +### Response + +- Success: standard success envelope with updated user data +- Failure example: + +```json +{ + "success": false, + "message": "Bad Request", + "error": { + "code": "BAD_REQUEST", + "message": "failed to update user" + } +} +``` + +--- + +## 6) Verify Profile + +- `GET /admin/v1/profile` + +Check: + +- `data.profile_image` is now populated with file URL. + +--- + +## Common Errors + +- `401 Unauthorized`: missing/expired token +- `413 Request Entity Too Large`: file too big +- `400 Unsupported file type` or validation error +- `404 File not found`: wrong `file_id` + +--- + +## Suggested Frontend Flow (Admin) + +1. Login -> save `access_token` +2. Upload image -> get `file_id` +3. Build URL `.../admin/v1/files/{file_id}/download` +4. Update profile with `profile_image` URL +5. Refresh profile screen via `GET /admin/v1/profile` + +--- + +## Customer: Company Documents Upload + +Customers can upload company documents using customer auth token. + +### Upload endpoint + +- `POST /api/v1/files/upload` + +### Recommended form-data for company docs + +- `file`: actual document file +- `category`: `company_documents` +- `tags`: repeatable. Suggested: + - `company` + - `document` + - `` (example: `tax_certificate`, `registration`, `license`) +- `description`: human-readable note, example: `Company tax certificate 2026` + +### Example curl + +```bash +CUSTOMER_TOKEN="" + +curl -X POST "http://localhost:8080/api/v1/files/upload" \ + -H "Authorization: Bearer ${CUSTOMER_TOKEN}" \ + -F "file=@/absolute/path/to/company-doc.pdf" \ + -F "category=company_documents" \ + -F "tags=company" \ + -F "tags=document" \ + -F "tags=tax_certificate" \ + -F "description=Company tax certificate 2026" +``` + +### Response + +```json +{ + "file_id": "69e8abcd2053abd3fa5433e1", + "filename": "company-doc.pdf", + "content_type": "application/pdf", + "size": 245632, + "message": "File uploaded successfully" +} +``` + +### Fetch company docs list + +Use list with category filter: + +- `GET /api/v1/files?category=company_documents&limit=50&skip=0` + +You can also filter by tags: + +- `GET /api/v1/files?category=company_documents&tags=tax_certificate` + +### Download company doc + +- `GET /api/v1/files/{file_id}/download` + +### Important current behavior + +- Upload works for customers and stores `uploaded_by` as `customer_id`. +- Company now supports `document_file_ids` for mapping GridFS files. + +### Attach uploaded documents to company + +Use admin company update endpoint after collecting uploaded `file_id` values: + +- `PUT /admin/v1/companies/{company_id}` + +Request body includes regular company fields plus `document_file_ids`: + +```json +{ + "name": "Opplens", + "type": "private", + "registration_number": "1234567890", + "tax_id": "1234567890", + "industry": "Technology", + "document_file_ids": [ + "69e8abcd2053abd3fa5433e1", + "69e8abcd2053abd3fa5433e2" + ] +} +``` + +`document_file_ids` is also returned in: + +- `GET /admin/v1/companies/{id}` +- `GET /api/v1/companies` (customer "my company" profile) + + diff --git a/src/app/auth/sign-in/page.tsx b/src/app/auth/sign-in/page.tsx index 6439105..21f9006 100644 --- a/src/app/auth/sign-in/page.tsx +++ b/src/app/auth/sign-in/page.tsx @@ -7,8 +7,10 @@ export const metadata: Metadata = { export default function SignIn() { return ( -
-
+
+
+
+
diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 9af0bb2..e3da265 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -14,17 +14,20 @@ export default function Page() { const hasProfileImage = data?.profile_image && !imageError; return ( -
-
+
+
+
+
Cover +
-
+
{hasProfileImage ? ( setImageError(true)} @@ -42,47 +45,63 @@ export default function Page() {
-

+

{data?.full_name}

-

+

@{data?.username}

-
+

User Information

-
-

Email

-

{data?.email}

+
+

+ Email +

+

+ {data?.email} +

-
-

Phone

-

+

+

+ Phone +

+

{data?.phone ? formatPhoneNumber(data?.phone) : "-"}

-
-

Department

-

+

+

+ Department +

+

{data?.department}

-
-

Position

-

{data?.position}

+
+

+ Position +

+

+ {data?.position} +

-
-

Role

-

+

+

+ Role +

+

{data?.role}

-
-

Status

+
+

+ Status +

{data?.status ?? "-"}
@@ -90,13 +109,15 @@ export default function Page() {
-
+

Account Details

-
-

Verified

+
+

+ Verified +

{data?.is_verified ? ( @@ -107,17 +128,19 @@ export default function Page() {
-
-

Last Login

-

+

+

+ Last Login +

+

{unixToDate({ unix: data?.last_login_at ?? 0, hasTime: true })}

-
-

+

+

Member Since

-

+

{unixToDate({ unix: data?.created_at ?? 0, hasTime: true })}

diff --git a/src/components/Auth/GoogleSigninButton.tsx b/src/components/Auth/GoogleSigninButton.tsx index 8b4526d..04315b8 100644 --- a/src/components/Auth/GoogleSigninButton.tsx +++ b/src/components/Auth/GoogleSigninButton.tsx @@ -2,7 +2,7 @@ import { GoogleIcon } from "@/assets/icons"; export default function GoogleSigninButton({ text }: { text: string }) { return ( - diff --git a/src/components/Auth/Signin/index.tsx b/src/components/Auth/Signin/index.tsx index 36f8120..53280cc 100644 --- a/src/components/Auth/Signin/index.tsx +++ b/src/components/Auth/Signin/index.tsx @@ -4,13 +4,21 @@ import SigninWithPassword from "../SigninWithPassword"; export default function Signin() { return ( - <>
-
- +
+
+ +
+
+

+ Welcome back +

+

+ Sign in to continue to your dashboard +

+
- ); } diff --git a/src/components/Auth/SigninWithPassword.tsx b/src/components/Auth/SigninWithPassword.tsx index 0b22163..eb4848a 100644 --- a/src/components/Auth/SigninWithPassword.tsx +++ b/src/components/Auth/SigninWithPassword.tsx @@ -12,6 +12,7 @@ import { useState } from "react"; import { SubmitHandler, useForm } from "react-hook-form"; import InputGroup from "../FormElements/InputGroup"; import { ThemeToggleSwitch } from "../Layouts/header/theme-toggle"; +import { Button } from "../ui-elements/button"; export default function SigninWithPassword() { const isMutating = useIsMutating(); @@ -29,7 +30,7 @@ export default function SigninWithPassword() { }; return (
-
+
{ setPasswordFieldInputType( passwordFieldInputType === "password" ? "text" : "password", @@ -74,20 +75,24 @@ export default function SigninWithPassword() { />
-
+ ) : ( + "Sign in" + ) + } + variant="primary" + shape="rounded" + size="small" type="submit" disabled={!!isMutating} className={cn( - "flex w-full cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary p-4 font-medium text-white transition hover:bg-opacity-90", - isMutating && "cursor-not-allowed bg-primary/50", + "w-full rounded-xl bg-gradient-to-r from-primary to-primary/85 p-4 font-semibold shadow-theme-xs transition-all duration-300 hover:-translate-y-0.5 hover:opacity-95", + isMutating && "cursor-not-allowed opacity-70 hover:translate-y-0", )} - > - {isMutating ? ( -
- ) : ( - "Sign in" - )} - + />
); diff --git a/src/components/FormElements/InputGroup/index.tsx b/src/components/FormElements/InputGroup/index.tsx index fc3dc7d..206f06d 100644 --- a/src/components/FormElements/InputGroup/index.tsx +++ b/src/components/FormElements/InputGroup/index.tsx @@ -16,6 +16,7 @@ type InputGroupProps = { name: Path; label: string; type: HTMLInputTypeAttribute; + accept?: string; placeholder?: string; className?: string; fileStyleVariant?: "style1" | "style2"; @@ -38,6 +39,7 @@ const InputGroup = ({ label, type, placeholder, + accept, required, disabled, active, @@ -60,7 +62,7 @@ const InputGroup = ({
); diff --git a/src/components/FormElements/InputGroup/tag-input.tsx b/src/components/FormElements/InputGroup/tag-input.tsx index ade2004..039d2a8 100644 --- a/src/components/FormElements/InputGroup/tag-input.tsx +++ b/src/components/FormElements/InputGroup/tag-input.tsx @@ -112,7 +112,7 @@ const TagInput = ({