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
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,13 @@ jobs:
with:
path: ./artifacts

- name: Create Draft Release 0.1.3
- name: Create Draft Release 0.1.3.1
uses: softprops/action-gh-release@v1
with:
tag_name: 0.1.3
name: Release 0.1.3
tag_name: 0.1.3.1
name: Release 0.1.3.1
body: |
Automated release for Nginx WAF Desktop Client v0.1.3.
Automated release for Nginx WAF Desktop Client v0.1.3.1.

## Changes
- Cross-platform builds for Linux x86_64, Windows x86_64, Mac Intel, Mac ARM.
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "Nginx_WAF"
version = "0.1.3"
version = "0.1.4"
description = "Nginx WAF - Advanced Nginx Management Platform"
authors = ["TinyActive"]
edition = "2024"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Nginx WAF",
"version": "0.1.3",
"version": "0.1.4",
"identifier": "com.tinyactive.nginx-waf",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
96 changes: 90 additions & 6 deletions src/components/domains/DomainDialogV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { Plus, Trash2, HelpCircle, Shield, Server, Settings } from 'lucide-react';
import { Plus, Trash2, HelpCircle, Shield, Server, Settings, RefreshCw } from 'lucide-react';
import { Domain } from '@/types';
import { toast } from 'sonner';

Expand Down Expand Up @@ -58,6 +58,8 @@ interface FormData {

// Security
modsecEnabled: boolean;
autoCreateSSL: boolean;
sslEmail: string;
realIpEnabled: boolean;
realIpCloudflare: boolean;
healthCheckEnabled: boolean;
Expand All @@ -77,9 +79,10 @@ interface DomainDialogV2Props {
onOpenChange: (open: boolean) => void;
domain?: Domain | null;
onSave: (domain: any) => void;
isLoading?: boolean;
}

export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDialogV2Props) {
export function DomainDialogV2({ open, onOpenChange, domain, onSave, isLoading = false }: DomainDialogV2Props) {
const {
register,
handleSubmit,
Expand All @@ -95,6 +98,8 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
lbAlgorithm: 'round_robin',
upstreams: [{ host: '', port: 80, protocol: 'http', sslVerify: true, weight: 1, maxFails: 3, failTimeout: 30 }],
modsecEnabled: true,
autoCreateSSL: false,
sslEmail: '',
realIpEnabled: false,
realIpCloudflare: false,
healthCheckEnabled: true,
Expand All @@ -119,6 +124,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
});

const realIpEnabled = watch('realIpEnabled');
const autoCreateSSL = watch('autoCreateSSL');
const healthCheckEnabled = watch('healthCheckEnabled');

// Reset form when dialog opens or domain changes
Expand Down Expand Up @@ -161,6 +167,8 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
lbAlgorithm: 'round_robin',
upstreams: [{ host: '', port: 80, protocol: 'http', sslVerify: true, weight: 1, maxFails: 3, failTimeout: 30 }],
modsecEnabled: true,
autoCreateSSL: false,
sslEmail: '',
realIpEnabled: false,
realIpCloudflare: false,
healthCheckEnabled: true,
Expand All @@ -187,8 +195,14 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
return;
}

// Validate SSL email if auto-create is enabled
if (data.autoCreateSSL && !data.sslEmail) {
toast.error('Email is required when auto-creating SSL certificate');
return;
}
Comment on lines +198 to +202

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This manual validation for sslEmail is redundant because react-hook-form is already configured with a conditional required rule for this field. The form's onSubmit will only be called if all validations pass. You can remove this block to rely solely on react-hook-form for validation, which simplifies the code and avoids duplication of logic.


// Prepare data in API format
const domainData = {
const domainData: any = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using any for domainData weakens type safety and reduces maintainability. It would be better to define a proper type for the data payload. Since the payload structure depends on whether you're creating or updating a domain, you could define a more comprehensive local type for domainData that covers all possible properties.

name: data.name,
status: data.status,
modsecEnabled: data.modsecEnabled,
Expand Down Expand Up @@ -221,6 +235,12 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
},
};

// Add SSL auto-creation fields only when creating new domain
if (!domain && data.autoCreateSSL) {
domainData.autoCreateSSL = true;
domainData.sslEmail = data.sslEmail;
}

onSave(domainData);
// Do not close dialog here - let parent component handle it after successful save
};
Expand Down Expand Up @@ -260,6 +280,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
id="name"
{...register('name', { required: 'Domain name is required' })}
placeholder="example.com"
disabled={isLoading}
/>
{errors.name && (
<p className="text-sm text-destructive">{errors.name.message}</p>
Expand All @@ -272,6 +293,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
<Select
value={watch('status')}
onValueChange={(value) => setValue('status', value as any)}
disabled={isLoading}
>
<SelectTrigger>
<SelectValue />
Expand All @@ -288,6 +310,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
<Select
value={watch('lbAlgorithm')}
onValueChange={(value) => setValue('lbAlgorithm', value as any)}
disabled={isLoading}
>
<SelectTrigger>
<SelectValue />
Expand All @@ -309,6 +332,7 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
variant="outline"
size="sm"
onClick={() => appendUpstream({ host: '', port: 80, protocol: 'http', sslVerify: true, weight: 1, maxFails: 3, failTimeout: 30 })}
disabled={isLoading}
>
<Plus className="h-4 w-4 mr-1" />
Add Backend
Expand Down Expand Up @@ -448,6 +472,54 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
/>
</div>

{!domain && (
<>
<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<Label htmlFor="autoSSL">Auto-create SSL Certificate</Label>
<p className="text-sm text-muted-foreground">
Automatically issue Let's Encrypt/ZeroSSL certificate after creating domain
</p>
</div>
<Controller
name="autoCreateSSL"
control={control}
render={({ field }) => (
<Switch
id="autoSSL"
checked={field.value}
onCheckedChange={field.onChange}
/>
)}
/>
</div>

{autoCreateSSL && (
<div className="ml-4 border-l-2 pl-4 space-y-2">
<Label htmlFor="sslEmail">Email for SSL Certificate *</Label>
<Input
id="sslEmail"
type="email"
{...register('sslEmail', {
required: autoCreateSSL ? 'Email is required for SSL certificate' : false,
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address'
}
})}
placeholder="admin@example.com"
/>
{errors.sslEmail && (
<p className="text-sm text-destructive">{errors.sslEmail.message}</p>
)}
<p className="text-xs text-muted-foreground">
This email will be used for SSL certificate notifications and renewal
</p>
</div>
)}
</>
)}

<div className="flex items-center justify-between p-3 border rounded-lg">
<div>
<Label htmlFor="realIp">Get Real Client IP from Proxy Headers</Label>
Expand Down Expand Up @@ -929,11 +1001,23 @@ export function DomainDialogV2({ open, onOpenChange, domain, onSave }: DomainDia
</Tabs>

<DialogFooter className="mt-6">
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
Cancel
</Button>
<Button type="submit">
{domain ? 'Update Domain' : 'Create Domain'}
<Button type="submit" disabled={isLoading}>
{isLoading ? (
<>
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
{domain ? 'Updating...' : 'Creating...'}
</>
) : (
domain ? 'Update Domain' : 'Create Domain'
)}
</Button>
</DialogFooter>
</form>
Expand Down
1 change: 1 addition & 0 deletions src/components/pages/Domains.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ export default function Domains() {
}}
onSave={handleSave}
domain={editingDomain}
isLoading={createDomain.isPending || updateDomain.isPending}
/>

<Suspense fallback={<SkeletonTable rows={5} columns={6} title="Domains" />}>
Expand Down
34 changes: 28 additions & 6 deletions src/components/pages/SSLTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,30 @@ export function SSLTable() {
certificateId: '',
});

const handleRenew = async (id: string) => {
const handleRenew = async (id: string, daysUntilExpiry?: number) => {
try {
setRenewingId(id);

// Check if certificate is eligible for renewal
if (daysUntilExpiry !== undefined && daysUntilExpiry > 30) {
toast.warning(
`Certificate is not yet eligible for renewal. It expires in ${daysUntilExpiry} days. Renewal is only allowed when less than 30 days remain.`
);
setRenewingId(null);
return;
}
Comment on lines +43 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The number 30 is a "magic number" representing the renewal eligibility period in days. It's used here and later in the component for the button's title attribute (line 172). To improve readability and maintainability, it's better to extract this value into a named constant at the top of the component, e.g., const RENEWAL_ELIGIBILITY_DAYS = 30;.


await renewMutation.mutateAsync(id);
toast.success('Certificate renewed successfully');
toast.success('SSL certificate renewed successfully! The new certificate has been applied.');
} catch (error: any) {
toast.error(error.response?.data?.message || 'Failed to renew certificate');
const errorMessage = error.response?.data?.message || 'Failed to renew certificate';

// Check if error is about eligibility
if (errorMessage.includes('not yet eligible') || errorMessage.includes('less than 30 days')) {
toast.warning(errorMessage);
} else {
toast.error(errorMessage);
}
} finally {
setRenewingId(null);
}
Expand Down Expand Up @@ -108,7 +125,7 @@ export function SSLTable() {
No SSL certificates found. Add one to get started.
</div>
<div className="text-sm text-muted-foreground">
You can issue a free Let's Encrypt certificate or upload a manual certificate for your domains.
You can issue a free ZeroSSL/Let's Encrypt certificate or upload a manual certificate for your domains.
</div>
</div>
) : (
Expand Down Expand Up @@ -145,12 +162,17 @@ export function SSLTable() {
<TableCell>{getStatusBadge(cert.status)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{cert.issuer === "Let's Encrypt" && (
{(cert.issuer === "Let's Encrypt" || cert.issuer === "ZeroSSL") && (
<Button
variant="outline"
size="sm"
onClick={() => handleRenew(cert.id)}
onClick={() => handleRenew(cert.id, cert.daysUntilExpiry)}
disabled={renewingId === cert.id}
title={
cert.daysUntilExpiry !== undefined && cert.daysUntilExpiry > 30
? `Certificate expires in ${cert.daysUntilExpiry} days. Renewal available when less than 30 days remain.`
: 'Renew SSL certificate'
}
>
{renewingId === cert.id ? (
<RefreshCw className="h-4 w-4 animate-spin" />
Expand Down
Loading
Loading