mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
feat: remove the donation reminder and its column
This commit is contained in:
1 parent
0b41672a3c
commit
abdbb34ce2
18 files changed
+14010
-105
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,13 @@
|
|||||||
"when": 1791388888997,
|
"when": 1791388888997,
|
||||||
"tag": "0069_plugin_channel",
|
"tag": "0069_plugin_channel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1791550497926,
|
||||||
|
"tag": "0070_drop_donation_modal",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "users" DROP COLUMN "donation_modal_dismissed";
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -491,6 +491,13 @@
|
|||||||
"when": 1791388888397,
|
"when": 1791388888397,
|
||||||
"tag": "0069_plugin_channel",
|
"tag": "0069_plugin_channel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 70,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791550496661,
|
||||||
|
"tag": "0070_drop_donation_modal",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `users` DROP COLUMN `donation_modal_dismissed`;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -456,6 +456,13 @@
|
|||||||
"when": 1791388887785,
|
"when": 1791388887785,
|
||||||
"tag": "0064_plugin_channel",
|
"tag": "0064_plugin_channel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 65,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1791550495357,
|
||||||
|
"tag": "0065_drop_donation_modal",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -242,8 +242,7 @@ async function initializeCompleteDatabase(): Promise<void> {
|
|||||||
username TEXT NOT NULL,
|
username TEXT NOT NULL,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||||
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
donation_modal_dismissed INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
@@ -970,9 +969,7 @@ const migrateSchema = () => {
|
|||||||
// CURRENT_TIMESTAMP, so the column is added empty and backfilled below.
|
// CURRENT_TIMESTAMP, so the column is added empty and backfilled below.
|
||||||
addColumnIfNotExists("users", "registered_at", "TEXT");
|
addColumnIfNotExists("users", "registered_at", "TEXT");
|
||||||
if (!hadRegisteredAtColumn) {
|
if (!hadRegisteredAtColumn) {
|
||||||
// Pre-existing users are backdated past the 30 day mark so they see the
|
// Pre-existing users get a date in the past, since their real one is unknown.
|
||||||
// donation modal immediately on upgrade instead of waiting a fresh
|
|
||||||
// 30 days as if they had just registered.
|
|
||||||
try {
|
try {
|
||||||
sqlite.exec(
|
sqlite.exec(
|
||||||
`UPDATE users SET registered_at = datetime('now', '-31 days') WHERE registered_at IS NULL`,
|
`UPDATE users SET registered_at = datetime('now', '-31 days') WHERE registered_at IS NULL`,
|
||||||
@@ -1000,12 +997,6 @@ const migrateSchema = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addColumnIfNotExists(
|
|
||||||
"users",
|
|
||||||
"donation_modal_dismissed",
|
|
||||||
"INTEGER NOT NULL DEFAULT 0",
|
|
||||||
);
|
|
||||||
|
|
||||||
addColumnIfNotExists("sessions", "external_session_ref", "TEXT");
|
addColumnIfNotExists("sessions", "external_session_ref", "TEXT");
|
||||||
|
|
||||||
addColumnIfNotExists("ssh_data", "name", "TEXT");
|
addColumnIfNotExists("ssh_data", "name", "TEXT");
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export const RETIRED_COLUMNS: Record<string, string[]> = {
|
|||||||
"identifier_path",
|
"identifier_path",
|
||||||
"name_path",
|
"name_path",
|
||||||
"scopes",
|
"scopes",
|
||||||
|
"donation_modal_dismissed",
|
||||||
],
|
],
|
||||||
sessions: ["oidc_sub", "oidc_sid", "sso_provider_id"],
|
sessions: ["oidc_sub", "oidc_sid", "sso_provider_id"],
|
||||||
user_preferences: [
|
user_preferences: [
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export const users = mysqlTable("users", {
|
|||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
isAdmin: boolean("is_admin").notNull().default(false),
|
isAdmin: boolean("is_admin").notNull().default(false),
|
||||||
registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`),
|
registeredAt: text("registered_at").notNull().default(sql`(CURRENT_TIMESTAMP)`),
|
||||||
donationModalDismissed: boolean("donation_modal_dismissed")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = mysqlTable("settings", {
|
export const settings = mysqlTable("settings", {
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ export const users = pgTable("users", {
|
|||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
isAdmin: boolean("is_admin").notNull().default(false),
|
isAdmin: boolean("is_admin").notNull().default(false),
|
||||||
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
donationModalDismissed: boolean("donation_modal_dismissed")
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = pgTable("settings", {
|
export const settings = pgTable("settings", {
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ export const users = sqliteTable("users", {
|
|||||||
passwordHash: text("password_hash").notNull(),
|
passwordHash: text("password_hash").notNull(),
|
||||||
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
|
isAdmin: integer("is_admin", { mode: "boolean" }).notNull().default(false),
|
||||||
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
registeredAt: text("registered_at").notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||||
donationModalDismissed: integer("donation_modal_dismissed", {
|
|
||||||
mode: "boolean",
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settings = sqliteTable("settings", {
|
export const settings = sqliteTable("settings", {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
isNativeTokenExportRequest,
|
isNativeTokenExportRequest,
|
||||||
resolveDesktopAutoSessionUser,
|
resolveDesktopAutoSessionUser,
|
||||||
} from "./desktop-auto-session.js";
|
} from "./desktop-auto-session.js";
|
||||||
import { shouldShowDonationModal } from "./donation-modal-utils.js";
|
|
||||||
import { PermissionManager } from "../../utils/permission-manager.js";
|
import { PermissionManager } from "../../utils/permission-manager.js";
|
||||||
import { registerUserApiKeyRoutes } from "./user-api-key-routes.js";
|
import { registerUserApiKeyRoutes } from "./user-api-key-routes.js";
|
||||||
import { registerBrandingRoutes } from "./branding-routes.js";
|
import { registerBrandingRoutes } from "./branding-routes.js";
|
||||||
@@ -595,11 +594,6 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
const hasPassword = user.passwordHash && user.passwordHash.trim() !== "";
|
const hasPassword = user.passwordHash && user.passwordHash.trim() !== "";
|
||||||
const isDualAuth = hasPassword && isExternalAccount(user);
|
const isDualAuth = hasPassword && isExternalAccount(user);
|
||||||
|
|
||||||
const showDonationModal = shouldShowDonationModal(
|
|
||||||
user.registeredAt,
|
|
||||||
!!user.donationModalDismissed,
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
@@ -610,7 +604,6 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor(
|
totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor(
|
||||||
user.id,
|
user.id,
|
||||||
),
|
),
|
||||||
show_donation_modal: showDonationModal,
|
|
||||||
linked: await describeDesktopLink(user.id),
|
linked: await describeDesktopLink(user.id),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -619,48 +612,6 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @openapi
|
|
||||||
* /users/me/dismiss-donation-modal:
|
|
||||||
* post:
|
|
||||||
* summary: Permanently dismiss the donation reminder modal
|
|
||||||
* description: Marks the donation reminder modal as dismissed for the currently authenticated user so it is never shown to them again.
|
|
||||||
* tags:
|
|
||||||
* - Users
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Donation modal dismissed.
|
|
||||||
* 401:
|
|
||||||
* description: Invalid userId or user not found.
|
|
||||||
* 500:
|
|
||||||
* description: Failed to dismiss donation modal.
|
|
||||||
*/
|
|
||||||
router.post(
|
|
||||||
"/me/dismiss-donation-modal",
|
|
||||||
authenticateJWT,
|
|
||||||
async (req: Request, res: Response) => {
|
|
||||||
const userId = (req as AuthenticatedRequest).userId;
|
|
||||||
|
|
||||||
if (!isNonEmptyString(userId)) {
|
|
||||||
return res.status(401).json({ error: "Invalid userId" });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const updated = await createCurrentUserRepository().update(userId, {
|
|
||||||
donationModalDismissed: true,
|
|
||||||
});
|
|
||||||
if (!updated) {
|
|
||||||
return res.status(401).json({ error: "User not found" });
|
|
||||||
}
|
|
||||||
return res.json({ success: true });
|
|
||||||
} catch (err) {
|
|
||||||
authLogger.error("Failed to dismiss donation modal", err);
|
|
||||||
return res
|
|
||||||
.status(500)
|
|
||||||
.json({ error: "Failed to dismiss donation modal" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* /users/me/token:
|
* /users/me/token:
|
||||||
|
|||||||
+2
-18
@@ -137,7 +137,6 @@ import {
|
|||||||
getActiveSessions,
|
getActiveSessions,
|
||||||
getUserPreferences,
|
getUserPreferences,
|
||||||
saveUserPreferences,
|
saveUserPreferences,
|
||||||
dismissDonationModal,
|
|
||||||
type UserPreferences,
|
type UserPreferences,
|
||||||
type OpenTabRecord,
|
type OpenTabRecord,
|
||||||
} from "@/main-axios";
|
} from "@/main-axios";
|
||||||
@@ -147,7 +146,6 @@ import {
|
|||||||
resolveLayoutTabTarget,
|
resolveLayoutTabTarget,
|
||||||
snapshotData,
|
snapshotData,
|
||||||
} from "@/shell/shell-layout";
|
} from "@/shell/shell-layout";
|
||||||
import { DonationReminderModal } from "@/user/DonationReminderModal.tsx";
|
|
||||||
import { useSyncStatus } from "@/hooks/use-sync-status";
|
import { useSyncStatus } from "@/hooks/use-sync-status";
|
||||||
import { remScale } from "@/lib/rem";
|
import { remScale } from "@/lib/rem";
|
||||||
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
||||||
@@ -304,8 +302,6 @@ export function AppShell({
|
|||||||
// The standalone desktop backend still owns system settings such as the
|
// The standalone desktop backend still owns system settings such as the
|
||||||
// Tailscale API key, even though it has only one implicit user.
|
// Tailscale API key, even though it has only one implicit user.
|
||||||
const showAdminUI = isAdmin;
|
const showAdminUI = isAdmin;
|
||||||
const [showDonationModal, setShowDonationModal] = useState(false);
|
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false);
|
|
||||||
const [backgroundTabRecords, setBackgroundTabRecords] = useState<
|
const [backgroundTabRecords, setBackgroundTabRecords] = useState<
|
||||||
OpenTabRecord[]
|
OpenTabRecord[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -444,18 +440,10 @@ export function AppShell({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserInfo()
|
getUserInfo()
|
||||||
.then((info) => {
|
.then((info) => setIsAdmin(info.is_admin))
|
||||||
setIsAdmin(info.is_admin);
|
|
||||||
setShowDonationModal(!!info.show_donation_modal);
|
|
||||||
})
|
|
||||||
.catch(() => setIsAdmin(false));
|
.catch(() => setIsAdmin(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDismissDonationModal = useCallback(() => {
|
|
||||||
setShowDonationModal(false);
|
|
||||||
dismissDonationModal().catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleAppFullscreen = useCallback(async () => {
|
const toggleAppFullscreen = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
@@ -3192,11 +3180,7 @@ export function AppShell({
|
|||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
<OnboardingHost onOpenChange={setShowOnboarding} />
|
<OnboardingHost />
|
||||||
<DonationReminderModal
|
|
||||||
open={showDonationModal && !showOnboarding}
|
|
||||||
onDismiss={handleDismissDonationModal}
|
|
||||||
/>
|
|
||||||
</ServerStatusProvider>
|
</ServerStatusProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1130,13 +1130,6 @@
|
|||||||
"customizeLayout": "Customize Dashboard",
|
"customizeLayout": "Customize Dashboard",
|
||||||
"justNow": "just now"
|
"justNow": "just now"
|
||||||
},
|
},
|
||||||
"donation": {
|
|
||||||
"title": "Enjoying Termix?",
|
|
||||||
"body": "Termix is free and open source, built and maintained by a two-person team in our spare time. If it's replaced a commercial tool you'd otherwise be paying for, a donation helps cover hosting costs and keeps development going. As of now, donations are crypto only.",
|
|
||||||
"milestones": "Donations also help fund the time to research and learn what's needed to build features like SAML and Kubernetes support. See the progress on the donate page.",
|
|
||||||
"cta": "Donate",
|
|
||||||
"dismiss": "Maybe later"
|
|
||||||
},
|
|
||||||
"dashboardTab": {
|
"dashboardTab": {
|
||||||
"stable": "STABLE",
|
"stable": "STABLE",
|
||||||
"registerNewServer": "Register a new server",
|
"registerNewServer": "Register a new server",
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ export interface UserInfo {
|
|||||||
is_dual_auth?: boolean;
|
is_dual_auth?: boolean;
|
||||||
password_hash?: string;
|
password_hash?: string;
|
||||||
data_unlocked?: boolean;
|
data_unlocked?: boolean;
|
||||||
show_donation_modal?: boolean;
|
|
||||||
/** On a desktop linked to a server, the account it is signed in to there. */
|
/** On a desktop linked to a server, the account it is signed in to there. */
|
||||||
linked?: LinkedAccountInfo | null;
|
linked?: LinkedAccountInfo | null;
|
||||||
}
|
}
|
||||||
@@ -1018,14 +1017,6 @@ export async function getUserInfo(): Promise<UserInfo> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function dismissDonationModal(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await authApi.post("/users/me/dismiss-donation-modal");
|
|
||||||
} catch (error) {
|
|
||||||
handleApiError(error, "dismiss donation modal");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCurrentToken(): Promise<string | null> {
|
export async function getCurrentToken(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const response = await authApi.get("/users/me/token");
|
const response = await authApi.get("/users/me/token");
|
||||||
|
|||||||
Reference in new issue
Block a user