mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: show the linked account's name and admin flag in the app (#1580)
This commit is contained in:
1 parent
e284c95891
commit
3caf6cca62
5 files changed
+118
-33
No files matched your search
@@ -27,6 +27,7 @@ import { registerUserSettingsRoutes } from "./user-settings-routes.js";
|
||||
import { registerTlsRoutes } from "./tls-routes.js";
|
||||
import { registerUserSessionRoutes } from "./user-session-routes.js";
|
||||
import { registerUserSocketTicketRoutes } from "./user-socket-ticket-routes.js";
|
||||
import { shownIdentity } from "../../sync/shown-identity.js";
|
||||
import { registerUserExternalAccountRoutes } from "./user-external-account-routes.js";
|
||||
import { registerUserPasswordResetRoutes } from "./user-password-reset-routes.js";
|
||||
import { registerUserAdminRoutes } from "./user-admin-routes.js";
|
||||
@@ -568,30 +569,6 @@ router.post("/logout", authenticateJWT, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* On a desktop linked to a server, the account it is signed in to there.
|
||||
* That account is who the user is; the local profile only mirrors it.
|
||||
*/
|
||||
async function describeDesktopLink(userId: string) {
|
||||
if (process.env.ELECTRON_EMBEDDED !== "true") return null;
|
||||
try {
|
||||
const { getLink } = await import("../../sync/client/link-store.js");
|
||||
const link = await getLink(userId);
|
||||
if (!link) return null;
|
||||
return {
|
||||
serverUrl: link.serverUrl,
|
||||
serverName: link.serverName,
|
||||
username: link.account?.username ?? link.remoteUsername,
|
||||
isAdmin: !!link.account?.isAdmin,
|
||||
roles: link.account?.roles ?? [],
|
||||
permissions: link.account?.permissions ?? [],
|
||||
status: link.status,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /users/me:
|
||||
@@ -624,18 +601,19 @@ router.get("/me", authenticateJWT, async (req: Request, res: Response) => {
|
||||
|
||||
const hasPassword = user.passwordHash && user.passwordHash.trim() !== "";
|
||||
const isDualAuth = hasPassword && isExternalAccount(user);
|
||||
const shown = await shownIdentity(user);
|
||||
|
||||
res.json({
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
is_admin: !!user.isAdmin,
|
||||
username: shown.username,
|
||||
is_admin: shown.isAdmin,
|
||||
is_external: isExternalAccount(user),
|
||||
is_dual_auth: isDualAuth,
|
||||
// Any second factor; the name is what 2.8 clients read.
|
||||
totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor(
|
||||
user.id,
|
||||
),
|
||||
linked: await describeDesktopLink(user.id),
|
||||
linked: shown.linked,
|
||||
// "second-factor-setup" while the login policy wants a factor set up.
|
||||
restriction: (req as AuthenticatedRequest).sessionRestriction ?? null,
|
||||
});
|
||||
@@ -758,10 +736,11 @@ router.post("/internal/auto-session", async (req, res) => {
|
||||
if (existingToken) {
|
||||
const existingPayload = await authManager.verifyJWTToken(existingToken);
|
||||
if (existingPayload?.userId === userRecord.id) {
|
||||
const shownNow = await shownIdentity(userRecord);
|
||||
return res.json({
|
||||
success: true,
|
||||
is_admin: !!userRecord.isAdmin,
|
||||
username: userRecord.username,
|
||||
is_admin: shownNow.isAdmin,
|
||||
username: shownNow.username,
|
||||
token: existingToken,
|
||||
});
|
||||
}
|
||||
@@ -773,10 +752,11 @@ router.post("/internal/auto-session", async (req, res) => {
|
||||
rememberMe: true,
|
||||
});
|
||||
|
||||
const shown = await shownIdentity(userRecord);
|
||||
const response = {
|
||||
success: true,
|
||||
is_admin: !!userRecord.isAdmin,
|
||||
username: userRecord.username,
|
||||
is_admin: shown.isAdmin,
|
||||
username: shown.username,
|
||||
token,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* On a desktop linked to a server, the account it is signed in to there.
|
||||
* That account is who the user is; the local profile only mirrors it.
|
||||
*/
|
||||
export async function describeDesktopLink(userId: string) {
|
||||
if (process.env.ELECTRON_EMBEDDED !== "true") return null;
|
||||
try {
|
||||
const { getLink } = await import("./client/link-store.js");
|
||||
const link = await getLink(userId);
|
||||
if (!link) return null;
|
||||
return {
|
||||
serverUrl: link.serverUrl,
|
||||
serverName: link.serverName,
|
||||
username: link.account?.username ?? link.remoteUsername,
|
||||
isAdmin: !!link.account?.isAdmin,
|
||||
roles: link.account?.roles ?? [],
|
||||
permissions: link.account?.permissions ?? [],
|
||||
status: link.status,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Who the app shows. On a desktop linked to a server that is the account it
|
||||
* is signed in to there, not the local user that holds the copy of its data.
|
||||
*/
|
||||
export async function shownIdentity(user: {
|
||||
id: string;
|
||||
username: string;
|
||||
isAdmin: boolean | null;
|
||||
}) {
|
||||
const linked = await describeDesktopLink(user.id);
|
||||
return {
|
||||
linked,
|
||||
username: linked?.username || user.username,
|
||||
isAdmin: linked ? linked.isAdmin : !!user.isAdmin,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getLink = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../../sync/client/link-store.js", () => ({ getLink }));
|
||||
|
||||
const { shownIdentity } = await import("../../sync/shown-identity.js");
|
||||
|
||||
const local = { id: "u1", username: "server-ab12", isAdmin: true };
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.ELECTRON_EMBEDDED = "true";
|
||||
getLink.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.ELECTRON_EMBEDDED;
|
||||
});
|
||||
|
||||
describe("who the app shows", () => {
|
||||
it("shows the local user when nothing is linked", async () => {
|
||||
getLink.mockResolvedValue(null);
|
||||
expect(await shownIdentity(local)).toMatchObject({
|
||||
username: "server-ab12",
|
||||
isAdmin: true,
|
||||
linked: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the account on the server once linked, with its own admin flag", async () => {
|
||||
getLink.mockResolvedValue({
|
||||
serverUrl: "https://home.example",
|
||||
serverName: "Home",
|
||||
remoteUsername: "luke",
|
||||
account: { username: "luke", isAdmin: false },
|
||||
status: "idle",
|
||||
});
|
||||
expect(await shownIdentity(local)).toMatchObject({
|
||||
username: "luke",
|
||||
isAdmin: false,
|
||||
});
|
||||
expect(getLink).toHaveBeenCalledWith("u1");
|
||||
});
|
||||
|
||||
it("shows the sign in name until the account has synced", async () => {
|
||||
getLink.mockResolvedValue({
|
||||
serverUrl: "https://home.example",
|
||||
remoteUsername: "luke",
|
||||
account: null,
|
||||
status: "syncing",
|
||||
});
|
||||
expect((await shownIdentity(local)).username).toBe("luke");
|
||||
});
|
||||
|
||||
it("is the local user on a server", async () => {
|
||||
delete process.env.ELECTRON_EMBEDDED;
|
||||
expect((await shownIdentity(local)).username).toBe("server-ab12");
|
||||
expect(getLink).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+7
-1
@@ -14,6 +14,7 @@ import {
|
||||
Auth,
|
||||
getStoredAuth,
|
||||
clearStoredAuth,
|
||||
storeAuth,
|
||||
markDesktopManualLogout,
|
||||
clearDesktopManualLogout,
|
||||
} from "@/auth/Auth";
|
||||
@@ -195,7 +196,12 @@ function App() {
|
||||
if (phase !== "verifying") return;
|
||||
appReadyPromise
|
||||
.then(() => getUserInfo())
|
||||
.then(async () => {
|
||||
.then(async (info) => {
|
||||
// The stored name can be an earlier account, after a switch or rename.
|
||||
if (info.username) {
|
||||
setAuthUsername(info.username);
|
||||
storeAuth(info.username);
|
||||
}
|
||||
if (isElectron()) {
|
||||
try {
|
||||
const token = await getCurrentToken();
|
||||
|
||||
@@ -138,7 +138,7 @@ function hasDesktopManualLogout() {
|
||||
return localStorage.getItem(DESKTOP_MANUAL_LOGOUT_KEY) === "true";
|
||||
}
|
||||
|
||||
function storeAuth(username: string) {
|
||||
export function storeAuth(username: string) {
|
||||
localStorage.setItem(
|
||||
STORAGE_KEY,
|
||||
JSON.stringify({ loggedIn: true, username }),
|
||||
|
||||
Reference in new issue
Block a user