Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ jobs:
ghcr.io/${{ env.OWNER_LC }}/remitlend-frontend:staging-latest
target: runner

build-args: |
NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_RPC_URL=${{ vars.NEXT_PUBLIC_RPC_URL }}
NEXT_PUBLIC_NETWORK_PASSPHRASE=${{ vars.NEXT_PUBLIC_NETWORK_PASSPHRASE }}
NEXT_PUBLIC_EXPLORER_URL=${{ vars.NEXT_PUBLIC_EXPLORER_URL }}
NEXT_PUBLIC_MANAGER_CONTRACT_ID=${{ vars.NEXT_PUBLIC_MANAGER_CONTRACT_ID }}
NEXT_PUBLIC_POOL_CONTRACT_ID=${{ vars.NEXT_PUBLIC_POOL_CONTRACT_ID }}
NEXT_PUBLIC_GOVERNANCE_CONTRACT_ID=${{ vars.NEXT_PUBLIC_GOVERNANCE_CONTRACT_ID }}
NEXT_PUBLIC_NFT_CONTRACT_ID=${{ vars.NEXT_PUBLIC_NFT_CONTRACT_ID }}

- name: Run Trivy vulnerability scanner (HIGH - warn)
uses: aquasecurity/trivy-action@master
with:
Expand Down Expand Up @@ -95,6 +105,32 @@ jobs:
with:
sarif_file: 'trivy-results.sarif'

- name: Run Trivy vulnerability scanner for frontend (HIGH - warn)
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/${{ env.OWNER_LC }}/remitlend-frontend:staging-${{ github.sha }}'
format: 'table'
severity: 'HIGH'
exit-code: '0'
trivyignores: '.trivyignore'

- name: Run Trivy vulnerability scanner for frontend (CRITICAL - fail)
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/${{ env.OWNER_LC }}/remitlend-frontend:staging-${{ github.sha }}'
format: 'sarif'
output: 'frontend-trivy-results.sarif'
severity: 'CRITICAL'
exit-code: '1'
limit-severities-for-sarif: 'true'
trivyignores: '.trivyignore'

- name: Upload frontend Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'frontend-trivy-results.sarif'

- name: Smoke-test the backend staging image
run: |
set -e
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ services:
ports:
- "3000:3000"
environment:
- NEXT_PUBLIC_API_URL=http://localhost:3001

- API_URL=http://backend:3001
depends_on:
- backend
Expand Down
20 changes: 19 additions & 1 deletion frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,26 @@ ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["npm", "run", "dev"]

# Build Stage
FROM base AS builder

ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_RPC_URL
ARG NEXT_PUBLIC_NETWORK_PASSPHRASE
ARG NEXT_PUBLIC_EXPLORER_URL
ARG NEXT_PUBLIC_MANAGER_CONTRACT_ID
ARG NEXT_PUBLIC_POOL_CONTRACT_ID
ARG NEXT_PUBLIC_GOVERNANCE_CONTRACT_ID
ARG NEXT_PUBLIC_NFT_CONTRACT_ID

ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_RPC_URL=$NEXT_PUBLIC_RPC_URL
ENV NEXT_PUBLIC_NETWORK_PASSPHRASE=$NEXT_PUBLIC_NETWORK_PASSPHRASE
ENV NEXT_PUBLIC_EXPLORER_URL=$NEXT_PUBLIC_EXPLORER_URL
ENV NEXT_PUBLIC_MANAGER_CONTRACT_ID=$NEXT_PUBLIC_MANAGER_CONTRACT_ID
ENV NEXT_PUBLIC_POOL_CONTRACT_ID=$NEXT_PUBLIC_POOL_CONTRACT_ID
ENV NEXT_PUBLIC_GOVERNANCE_CONTRACT_ID=$NEXT_PUBLIC_GOVERNANCE_CONTRACT_ID
ENV NEXT_PUBLIC_NFT_CONTRACT_ID=$NEXT_PUBLIC_NFT_CONTRACT_ID

RUN npm run build

# Production Stage
Expand Down
83 changes: 54 additions & 29 deletions frontend/src/app/[locale]/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,15 @@ function Toggle({
</div>
<button
onClick={() => onChange(!checked)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
checked ? "bg-indigo-600" : "bg-zinc-300 dark:bg-zinc-700"
}`}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${checked ? "bg-indigo-600" : "bg-zinc-300 dark:bg-zinc-700"
}`}
role="switch"
aria-checked={checked}
aria-label={label}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
checked ? "translate-x-6" : "translate-x-1"
}`}
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${checked ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
</div>
Expand Down Expand Up @@ -222,11 +220,10 @@ function WalletSection() {
</p>
</div>
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${
network?.isSupported
? "bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400"
: "bg-yellow-50 text-yellow-700 dark:bg-yellow-500/10 dark:text-yellow-400"
}`}
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium ${network?.isSupported
? "bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400"
: "bg-yellow-50 text-yellow-700 dark:bg-yellow-500/10 dark:text-yellow-400"
}`}
>
<span className="h-1.5 w-1.5 rounded-full bg-current" />
{network?.isSupported ? "Supported" : "Unsupported"}
Expand Down Expand Up @@ -281,6 +278,7 @@ function NotificationsSection() {
});
const [saved, setSaved] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [phoneError, setPhoneError] = useState<string | null>(null);

useEffect(() => {
if (!data) return;
Expand Down Expand Up @@ -314,11 +312,30 @@ function NotificationsSection() {

const handleSave = () => {
setSaveError(null);
setPhoneError(null);

const phone = prefs.phone.trim();

if (prefs.sms) {
if (!phone) {
setPhoneError("A phone number is required for SMS notifications.");
return;
}

// Basic international phone validation
const phoneRegex = /^\+?[1-9]\d{7,14}$/;

if (!phoneRegex.test(phone)) {
setPhoneError("Please enter a valid phone number.");
return;
}
}

updateNotificationPreferences.mutate(
{
emailEnabled: prefs.email,
smsEnabled: prefs.sms,
phone: prefs.phone.trim() || null,
phone: phone || null,
perTypeOverrides,
},
{
Expand Down Expand Up @@ -361,7 +378,10 @@ function NotificationsSection() {
/>
<Toggle
checked={prefs.sms}
onChange={() => toggle("sms")}
onChange={() => {
setPhoneError(null);
toggle("sms");
}}
label="SMS Notifications"
description="Requires a verified phone number"
/>
Expand All @@ -370,13 +390,21 @@ function NotificationsSection() {
label="Phone number"
placeholder="+14155552671"
value={prefs.phone}
onChange={(e) => setPrefs((p) => ({ ...p, phone: e.target.value }))}
onChange={(e) => {
setPhoneError(null);
setPrefs((p) => ({ ...p, phone: e.target.value }));
}}
helperText={
prefs.sms
? "A phone number is required for SMS notifications."
: "Optional unless SMS notifications are enabled."
}
/>
{phoneError && (
<p className="mt-1 text-sm text-red-600 dark:text-red-400">
{phoneError}
</p>
)}
</div>

<div className="space-y-2">
Expand Down Expand Up @@ -471,11 +499,10 @@ function SecuritySection() {
<div className="flex justify-between text-sm">
<span className="text-zinc-500 dark:text-zinc-400">KYC Status</span>
<span
className={`font-medium ${
user?.kycVerified
? "text-green-600 dark:text-green-400"
: "text-yellow-600 dark:text-yellow-400"
}`}
className={`font-medium ${user?.kycVerified
? "text-green-600 dark:text-green-400"
: "text-yellow-600 dark:text-yellow-400"
}`}
>
{user?.kycVerified ? "Verified" : "Not Verified"}
</span>
Expand Down Expand Up @@ -565,11 +592,10 @@ function DisplaySection() {
<button
key={opt}
onClick={() => setTheme(opt)}
className={`px-3 py-1 rounded-lg text-sm font-medium transition-colors ${
active
? "bg-indigo-600 text-white"
: "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
}`}
className={`px-3 py-1 rounded-lg text-sm font-medium transition-colors ${active
? "bg-indigo-600 text-white"
: "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
}`}
>
{opt[0].toUpperCase() + opt.slice(1)}
</button>
Expand Down Expand Up @@ -653,11 +679,10 @@ export default function SettingsPage() {
<li key={id}>
<button
onClick={() => setActiveSection(id)}
className={`flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium w-full transition-colors whitespace-nowrap ${
activeSection === id
? "bg-indigo-50 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-400"
: "text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50"
}`}
className={`flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium w-full transition-colors whitespace-nowrap ${activeSection === id
? "bg-indigo-50 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-400"
: "text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-900 dark:hover:text-zinc-50"
}`}
>
<Icon className="h-4 w-4 flex-shrink-0" />
{label}
Expand Down
14 changes: 3 additions & 11 deletions frontend/src/app/components/remittance/RemittanceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,17 +164,10 @@ export function RemittanceForm({ onSuccess }: RemittanceFormProps) {
onChange={(e) => handleAddressChange(e.target.value)}
disabled={mutation.isPending}
required
className={errors.recipientAddress ? "border-red-600" : ""}
error={errors.recipientAddress || undefined}
helperText="Enter the recipient's Stellar public key (56 characters starting with G)"
/>

{errors.recipientAddress && (
<div className="mt-1 flex items-start gap-2 text-sm text-red-600">
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<span>{errors.recipientAddress}</span>
</div>
)}

{/* Token Selection */}
<div className="space-y-2">
<label className="block text-sm font-semibold text-zinc-900 dark:text-zinc-50">
Expand Down Expand Up @@ -229,9 +222,8 @@ export function RemittanceForm({ onSuccess }: RemittanceFormProps) {
disabled={mutation.isPending}
maxLength={28}
rows={2}
className={`w-full px-3 py-2 border rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-50 focus:outline-none focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-400 resize-none dark:border-zinc-700 ${
errors.memo ? "border-red-600" : "border-zinc-300"
}`}
className={`w-full px-3 py-2 border rounded-lg bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-50 focus:outline-none focus:ring-2 focus:ring-indigo-600 dark:focus:ring-indigo-400 resize-none dark:border-zinc-700 ${errors.memo ? "border-red-600" : "border-zinc-300"
}`}
/>
{errors.memo && (
<div className="flex items-start gap-2 text-sm text-red-600">
Expand Down
Loading