mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-25 21:25:06 +02:00
Merge branch 'dev' of github.com:fosrl/pangolin into dev
This commit is contained in:
commit
84c28645be
6 changed files with 88 additions and 71 deletions
|
@ -1270,5 +1270,7 @@
|
||||||
"createDomainSaveTheseRecords": "Save These Records",
|
"createDomainSaveTheseRecords": "Save These Records",
|
||||||
"createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.",
|
"createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.",
|
||||||
"createDomainDnsPropagation": "DNS Propagation",
|
"createDomainDnsPropagation": "DNS Propagation",
|
||||||
"createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings."
|
"createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.",
|
||||||
|
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
||||||
|
"resourcePortNotAllowed": "Port number should not be set for HTTP resources"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ export default async function migration() {
|
||||||
const db = new Database(location);
|
const db = new Database(location);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
db.pragma("foreign_keys = OFF");
|
||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE 'clientSites' (
|
CREATE TABLE 'clientSites' (
|
||||||
|
@ -99,8 +101,6 @@ export default async function migration() {
|
||||||
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
db.pragma("foreign_keys = OFF");
|
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE '__new_sites' (
|
CREATE TABLE '__new_sites' (
|
||||||
'siteId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
'siteId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
@ -135,8 +135,6 @@ export default async function migration() {
|
||||||
ALTER TABLE '__new_sites' RENAME TO 'sites';
|
ALTER TABLE '__new_sites' RENAME TO 'sites';
|
||||||
`);
|
`);
|
||||||
|
|
||||||
db.pragma("foreign_keys = ON");
|
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
ALTER TABLE 'domains' ADD 'type' text;
|
ALTER TABLE 'domains' ADD 'type' text;
|
||||||
ALTER TABLE 'domains' ADD 'verified' integer DEFAULT 0 NOT NULL;
|
ALTER TABLE 'domains' ADD 'verified' integer DEFAULT 0 NOT NULL;
|
||||||
|
@ -148,7 +146,10 @@ export default async function migration() {
|
||||||
ALTER TABLE 'user' ADD 'twoFactorSetupRequested' integer DEFAULT 0;
|
ALTER TABLE 'user' ADD 'twoFactorSetupRequested' integer DEFAULT 0;
|
||||||
ALTER TABLE 'resources' DROP COLUMN 'isBaseDomain';
|
ALTER TABLE 'resources' DROP COLUMN 'isBaseDomain';
|
||||||
`);
|
`);
|
||||||
})(); // <-- executes the transaction immediately
|
})();
|
||||||
|
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
console.log(`Migrated database schema`);
|
console.log(`Migrated database schema`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Unable to migrate database schema");
|
console.log("Unable to migrate database schema");
|
||||||
|
|
|
@ -122,7 +122,20 @@ export default function GeneralForm() {
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
subdomain: z.string().optional(),
|
subdomain: z.string().optional(),
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
domainId: z.string().optional()
|
domainId: z.string().optional(),
|
||||||
|
proxyPort: z.number().int().min(1).max(65535).optional()
|
||||||
|
}).refine((data) => {
|
||||||
|
// For non-HTTP resources, proxyPort should be defined
|
||||||
|
if (!resource.http) {
|
||||||
|
return data.proxyPort !== undefined;
|
||||||
|
}
|
||||||
|
// For HTTP resources, proxyPort should be undefined
|
||||||
|
return data.proxyPort === undefined;
|
||||||
|
}, {
|
||||||
|
message: !resource.http
|
||||||
|
? "Port number is required for non-HTTP resources"
|
||||||
|
: "Port number should not be set for HTTP resources",
|
||||||
|
path: ["proxyPort"]
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
@ -133,7 +146,8 @@ export default function GeneralForm() {
|
||||||
enabled: resource.enabled,
|
enabled: resource.enabled,
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||||
domainId: resource.domainId || undefined
|
domainId: resource.domainId || undefined,
|
||||||
|
proxyPort: resource.proxyPort || undefined
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
@ -196,7 +210,8 @@ export default function GeneralForm() {
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
domainId: data.domainId
|
domainId: data.domainId,
|
||||||
|
proxyPort: data.proxyPort
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -222,7 +237,8 @@ export default function GeneralForm() {
|
||||||
enabled: data.enabled,
|
enabled: data.enabled,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
fullDomain: resource.fullDomain
|
fullDomain: resource.fullDomain,
|
||||||
|
proxyPort: data.proxyPort
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
@ -333,6 +349,39 @@ export default function GeneralForm() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!resource.http && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="proxyPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
{t("resourcePortNumber")}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value
|
||||||
|
? parseInt(e.target.value)
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
{t("resourcePortNumberDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{resource.http && (
|
{resource.http && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Domain</Label>
|
<Label>Domain</Label>
|
||||||
|
|
|
@ -25,32 +25,34 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||||
} else if (type === "wireguard") {
|
} else if (type === "wireguard") {
|
||||||
return "WireGuard";
|
return "WireGuard";
|
||||||
} else if (type === "local") {
|
} else if (type === "local") {
|
||||||
return t('local');
|
return t("local");
|
||||||
} else {
|
} else {
|
||||||
return t('unknown');
|
return t("unknown");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert>
|
<Alert>
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">{t('siteInfo')}</AlertTitle>
|
<AlertTitle className="font-semibold">{t("siteInfo")}</AlertTitle>
|
||||||
<AlertDescription className="mt-4">
|
<AlertDescription className="mt-4">
|
||||||
<InfoSections cols={env.flags.enableClients ? 3 : 2}>
|
<InfoSections cols={env.flags.enableClients ? 3 : 2}>
|
||||||
{(site.type == "newt" || site.type == "wireguard") && (
|
{(site.type == "newt" || site.type == "wireguard") && (
|
||||||
<>
|
<>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t('status')}</InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
|
{t("status")}
|
||||||
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{site.online ? (
|
{site.online ? (
|
||||||
<div className="text-green-500 flex items-center space-x-2">
|
<div className="text-green-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||||
<span>{t('online')}</span>
|
<span>{t("online")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-neutral-500 flex items-center space-x-2">
|
<div className="text-neutral-500 flex items-center space-x-2">
|
||||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||||
<span>{t('offline')}</span>
|
<span>{t("offline")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
|
@ -58,17 +60,22 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t('connectionType')}</InfoSectionTitle>
|
<InfoSectionTitle>
|
||||||
|
{t("connectionType")}
|
||||||
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{getConnectionTypeString(site.type)}
|
{getConnectionTypeString(site.type)}
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
|
||||||
<InfoSectionTitle>Address</InfoSectionTitle>
|
{env.flags.enableClients && (
|
||||||
<InfoSectionContent>
|
<InfoSection>
|
||||||
{site.address?.split("/")[0]}
|
<InfoSectionTitle>Address</InfoSectionTitle>
|
||||||
</InfoSectionContent>
|
<InfoSectionContent>
|
||||||
</InfoSection>
|
{site.address?.split("/")[0]}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
)}
|
||||||
</InfoSections>
|
</InfoSections>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
|
@ -894,49 +894,6 @@ WantedBy=default.target`
|
||||||
)}
|
)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
id="create-site-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="copied"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="terms"
|
|
||||||
defaultChecked={
|
|
||||||
form.getValues(
|
|
||||||
"copied"
|
|
||||||
) as boolean
|
|
||||||
}
|
|
||||||
onCheckedChange={(
|
|
||||||
e
|
|
||||||
) => {
|
|
||||||
form.setValue(
|
|
||||||
"copied",
|
|
||||||
e as boolean
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="terms"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
"siteConfirmCopy"
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -63,6 +63,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [securityKeyLoading, setSecurityKeyLoading] = useState(false);
|
||||||
const hasIdp = idps && idps.length > 0;
|
const hasIdp = idps && idps.length > 0;
|
||||||
|
|
||||||
const [mfaRequested, setMfaRequested] = useState(false);
|
const [mfaRequested, setMfaRequested] = useState(false);
|
||||||
|
@ -98,7 +99,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
||||||
|
|
||||||
async function initiateSecurityKeyAuth() {
|
async function initiateSecurityKeyAuth() {
|
||||||
setShowSecurityKeyPrompt(true);
|
setShowSecurityKeyPrompt(true);
|
||||||
setLoading(true);
|
setSecurityKeyLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -117,7 +118,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
||||||
// Perform WebAuthn authentication
|
// Perform WebAuthn authentication
|
||||||
try {
|
try {
|
||||||
const credential = await startAuthentication(options);
|
const credential = await startAuthentication(options);
|
||||||
|
|
||||||
// Verify authentication
|
// Verify authentication
|
||||||
const verifyRes = await api.post(
|
const verifyRes = await api.post(
|
||||||
"/auth/security-key/authenticate/verify",
|
"/auth/security-key/authenticate/verify",
|
||||||
|
@ -167,7 +168,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setSecurityKeyLoading(false);
|
||||||
setShowSecurityKeyPrompt(false);
|
setShowSecurityKeyPrompt(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -432,8 +433,8 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={initiateSecurityKeyAuth}
|
onClick={initiateSecurityKeyAuth}
|
||||||
loading={loading}
|
loading={securityKeyLoading}
|
||||||
disabled={loading || showSecurityKeyPrompt}
|
disabled={securityKeyLoading || showSecurityKeyPrompt}
|
||||||
>
|
>
|
||||||
<FingerprintIcon className="w-4 h-4 mr-2" />
|
<FingerprintIcon className="w-4 h-4 mr-2" />
|
||||||
{t('securityKeyLogin', {
|
{t('securityKeyLogin', {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue