This commit is contained in:
Korbs 2025-02-20 01:47:01 -05:00
parent 60e6fa9a85
commit 086e539155
Signed by: Korbs
SSH key fingerprint: SHA256:Q0b0KraMldpAO9oKa+w+gcsXsOTykQ4UkAKn0ByGn5U
11 changed files with 289 additions and 55 deletions

9
.env.sample Executable file
View file

@ -0,0 +1,9 @@
BASE_URL="https://zv.example.org"
AUTH_SECRET="Ah6Lm3h8Qmnhm8oDfw09AOUYEOMKpzfqIFPjBA2waLPm9SD65BA1eSXde9OS80nE"
DATABASE_URL="Server=aptabase_db;Port=5432;User Id=aptabase;Password=0000000000000000000000000000000000000000000000000000000000000000;Database=aptabase"
CLICKHOUSE_URL="Host=aptabase_events_db;Port=8123;Username=aptabase;Password=0000000000000000000000000000000000000000000000000000000000000000"
SMTP_HOST="smtp.resend.com"
SMTP_PORT="465"
SMTP_USERNAME="resend"
SMTP_PASSWORD=""
SMTP_FROM_ADDRESS="no-reply@example.org"

3
.gitignore vendored
View file

@ -1,3 +1,4 @@
.env
src/node_modules
src/wwwroot/
src/wwwroot/
tmp/

View file

@ -4,6 +4,7 @@ A fork of Aptabase Server software, modified to only work with projects using Za
## Changes
- Updated app key generation to use `ZV` instead of `SH`
- Updated the name "Aptabase" to "Zalvena" in some sections
- Updated browserslist
- Updated `node:18` to `over/bun:1` in `Dockerfile`
- Updated the pacakge `@types/node` from `20.8.7` to `22.13.4`
@ -41,6 +42,8 @@ A fork of Aptabase Server software, modified to only work with projects using Za
- Updated the pacakge `rehype-highlight` from `7.0.0` to `7.0.2`
- Updated the pacakge `remark-gfm` from `4.0.0` to `4.0.1`
- Updated the pacakge `sonner` from `1.0.3` to `2.0.0`
- Added `.env.sample`
- Redesigned login screen
- Remote sources removed to make this fork comply with SudoVanilla Umbrella Policy
- Default avatar points to https://md.sudovanilla.org/images/icons/Aptabase.jpg
- Flags point to https://md.sudovanilla.org/images/flags/
@ -51,6 +54,21 @@ A fork of Aptabase Server software, modified to only work with projects using Za
- Removed `/tests/`
- Removed `/tools/`
## Planned Changes
- Revamp emails
- Use SudoVanilla Gateway email template
- Revamp design
- Use Mona Font
- Replace sidebar layout with header
- Replace Tabler icons with Iconoir
- Add SudoVanilla Gateway OpenID Login
- Add new instruction screens
- Remove all traces of Aptabase trademarks, branding, and name to differentiate Zalvena from Aptabase
- Remove all Aptabase Cloud functions including Billing, US/EU Regions, and more
**Maybe**
- Replace usage of React with Astro? 🤔
## License
Aptabase and Zalvena Service is open-source under the [AGPLv3 license](./LICENSE). You can use it for free, but you must share any changes you make to the code.

View file

@ -1,5 +1,5 @@
<p>Hi ##NAME##,</p>
<p>You asked for a magic link to Aptabase and here it is.</p>
<p>You asked for a magic link to Zalvena and here it is.</p>
<p>
<a target="_blank" rel="noopener noreferrer nofollow" href="##URL##">Confirm your registration</a>
</p>

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react",
"composite": true,
"module": "ES2022",
"moduleResolution": "Node",

View file

@ -12,7 +12,7 @@ export function Page(props: Props) {
const location = useLocation();
useEffect(() => {
document.title = `${props.title} · Aptabase`;
document.title = `${props.title} · Zalvena`;
}, [props.title]);
return (

View file

@ -80,7 +80,7 @@ export function AppSharing(props: Props) {
</ol>
<p>
<span className="font-bold">Note:</span> Aptabase won't send an email as part of the sharing process. We
<span className="font-bold">Note:</span> Zalvena won't send an email as part of the sharing process. We
recommend you getting in contact with them directly and asking them to sign up.
</p>
</AlertDescription>

View file

@ -0,0 +1,130 @@
import { requestSignInLink } from "@features/auth";
import { Page } from "@components/Page";
import { useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { DataResidency } from "./DataResidency";
import { LegalNotice } from "./LegalNotice";
import { RegionSwitch } from "./RegionSwitch";
import { SignInWithGitHub } from "./SignInWithGitHub";
import { SignInWithGoogle } from "./SignInWithGoogle";
import { isOAuthEnabled } from "@features/env";
import { Logo } from "./Logo";
import { Button } from "@components/Button";
import { TextInput } from "@components/TextInput";
type FormStatus = "idle" | "loading" | "success" | "notfound";
type StatusMessageProps = {
status: FormStatus;
};
const SignUpMessage = () => (
<span className="block">
Don't have an account?{" "}
<Link className="font-semibold text-foreground" to="/auth/register">
Sign up
</Link>{" "}
for free.
</span>
);
const StatusMessage = (props: StatusMessageProps) => {
if (props.status === "success") {
return <span className="text-success">Woo-hoo! Email sent, go check your inbox!</span>;
}
if (props.status === "notfound") {
return (
<>
<span className="text-destructive">Could not find an account with that email.</span>
<SignUpMessage />
</>
);
}
return <SignUpMessage />;
};
const RedirectErrorMessage = () => {
const [params] = useSearchParams();
const error = params.get("error");
if (!error) {
return null;
}
const message = error === "expired" ? "This link has expired." : "This link is invalid.";
return (
<p className="mx-auto text-center mb-10 text-destructive text-sm">
{message} Please request a new one.
</p>
);
};
Component.displayName = "LoginPage";
export function Component() {
const [email, setEmail] = useState("");
const [status, setStatus] = useState<FormStatus>("idle");
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus("loading");
const found = await requestSignInLink(email);
setStatus(found ? "success" : "notfound");
};
return (
<Page title="Login">
<div className="mx-auto text-center mb-10">
<RedirectErrorMessage />
</div>
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center">
<Logo className="mx-auto h-12 w-auto text-primary" />
<h2 className="text-center text-3xl text-foreground font-bold">Sign in to your account</h2>
<DataResidency />
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="py-8 px-4 sm:rounded-lg sm:px-10">
{isOAuthEnabled && (
<>
<div className="space-y-2">
<SignInWithGitHub />
<SignInWithGoogle />
</div>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-muted">OR</span>
</div>
</div>
</>
)}
<form onSubmit={handleSubmit} className="flex flex-col space-y-4">
<TextInput
label="Enter your email address"
name="email"
type="email"
placeholder="peter.parker@corp.com"
autoComplete="email"
value={email}
required={true}
onChange={(e) => setEmail(e.target.value)}
/>
<Button loading={status === "loading"}>Send magic link</Button>
<p className="text-center text-sm h-10 text-muted-foreground">
<StatusMessage status={status} />
</p>
</form>
</div>
<LegalNotice operation="signin" />
<RegionSwitch />
</div>
</Page>
);
}

View file

@ -1,14 +1,7 @@
import { requestSignInLink } from "@features/auth";
import { Page } from "@components/Page";
import { useState } from "react";
import React, { useState } from "react";
import { Link, useSearchParams } from "react-router-dom";
import { DataResidency } from "./DataResidency";
import { LegalNotice } from "./LegalNotice";
import { RegionSwitch } from "./RegionSwitch";
import { SignInWithGitHub } from "./SignInWithGitHub";
import { SignInWithGoogle } from "./SignInWithGoogle";
import { isOAuthEnabled } from "@features/env";
import { Logo } from "./Logo";
import { Button } from "@components/Button";
import { TextInput } from "@components/TextInput";
@ -30,13 +23,13 @@ const SignUpMessage = () => (
const StatusMessage = (props: StatusMessageProps) => {
if (props.status === "success") {
return <span className="text-success">Woo-hoo! Email sent, go check your inbox!</span>;
return <span className="text-success">The link has been sent to your inbox, check it now.</span>;
}
if (props.status === "notfound") {
return (
<>
<span className="text-destructive">Could not find an account with that email.</span>
<span className="text-destructive">An account with that email was not found, try again.</span>
<SignUpMessage />
</>
);
@ -56,7 +49,7 @@ const RedirectErrorMessage = () => {
return (
<p className="mx-auto text-center mb-10 text-destructive text-sm">
{message} Please request a new one.
{message} Link invalid, try again. Links only last 15 minutes.
</p>
);
};
@ -75,56 +68,44 @@ export function Component() {
};
return (
<Page title="Login">
<div className="mx-auto text-center mb-10">
<RedirectErrorMessage />
</div>
<div className="login">
<div className="login-box">
<div className="lb-start">
<img src="https://ocean.sudovanilla.org/media/images/wallpapers/Trenta%20OS%20%28Discontinued%29/mountains.jpg"/>
<p>Zalvena</p>
</div>
<div className="lb-end">
<p className="redir-error"><RedirectErrorMessage/></p>
<h2>Login to your account</h2>
<button id="sv-gw-disabled" class="generic-oauth">Login with SudoVanilla Gateway</button>
<div className="sm:mx-auto sm:w-full sm:max-w-md text-center">
<Logo className="mx-auto h-12 w-auto text-primary" />
<h2 className="text-center text-3xl text-foreground font-bold">Sign in to your account</h2>
<DataResidency />
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="py-8 px-4 sm:rounded-lg sm:px-10">
{isOAuthEnabled && (
<>
<div className="space-y-2">
<SignInWithGitHub />
<SignInWithGoogle />
</div>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-muted">OR</span>
</div>
</div>
<div className="relative my-4">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-muted">OR</span>
</div>
</div>
</>
)}
<form onSubmit={handleSubmit} className="flex flex-col space-y-4">
<form onSubmit={handleSubmit}>
<TextInput
label="Enter your email address"
label="Email Address"
name="email"
type="email"
placeholder="peter.parker@corp.com"
placeholder="nemo@example.org"
autoComplete="email"
value={email}
required={true}
onChange={(e) => setEmail(e.target.value)}
/>
<Button loading={status === "loading"}>Send magic link</Button>
<p className="text-center text-sm h-10 text-muted-foreground">
<StatusMessage status={status} />
</p>
<Button loading={status === "loading"}>Send Login Link</Button>
<p><StatusMessage status={status}/></p>
</form>
<hr/>
<p>By signing in, you agree to the Privacy Policy.</p>
</div>
<LegalNotice operation="signin" />
<RegionSwitch />
</div>
</Page>
);
</div>
)
}

View file

@ -48,7 +48,7 @@ export function ToolsList() {
icon="excel"
name="Microsoft Excel"
href="https://www.microsoft.com/en-ie/microsoft-365"
description="The most popular spreadsheet works great with Aptabase CSV files. Open it on Excel and start exploring your data with Pivot Tables and Charts."
description="The most popular spreadsheet works great with Zalvena CSV files. Open it on Excel and start exploring your data with Pivot Tables and Charts."
/>
<Tool

View file

@ -84,4 +84,98 @@
::-webkit-scrollbar-thumb:hover {
@apply bg-muted-foreground;
}
/* Login Page */
.login {
position: fixed;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background: linear-gradient(0deg, #1a1a1a,black);
}
.login-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
display: flex;
background: black;
border-radius: 12px;
border: 2px #242424 solid;
padding: 6px;
}
.lb-start {
width: 100%;
max-width: 524px;
position: relative;
}
.lb-start img {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
object-fit: cover;
object-position: bottom;
border-radius: 6px;
}
.lb-start p {
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
text-align: center;
align-content: center;
backdrop-filter: blur(6px) brightness(0.5);
border-radius: 6px;
font-size: 40px;
font-weight: bold;
letter-spacing: 2px;
font-family: 'Cal Sans';
}
.lb-end {
display: flex;
flex-direction: column;
padding: 48px 24px;
text-align: center;
width: 100%;
justify-content: center;
align-items: center;
}
.lb-end * {
max-width: 472px;
width: inherit;
}
.lb-end .relative.my-4 {
text-align: center;
max-width: max-content;
}
.generic-oauth {
border: 1px #1a1a1a solid;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
margin-top: 12px;
}
.lb-end form {
display: flex;
flex-direction: column;
gap: 12px;
}
.lb-end hr {
margin: 12px 0px;
}
#sv-gw-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.lb-end h2 {
text-align: center;
font-size: 18px;
margin-bottom: 14px;
}