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 { registerTlsRoutes } from "./tls-routes.js";
|
||||||
import { registerUserSessionRoutes } from "./user-session-routes.js";
|
import { registerUserSessionRoutes } from "./user-session-routes.js";
|
||||||
import { registerUserSocketTicketRoutes } from "./user-socket-ticket-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 { registerUserExternalAccountRoutes } from "./user-external-account-routes.js";
|
||||||
import { registerUserPasswordResetRoutes } from "./user-password-reset-routes.js";
|
import { registerUserPasswordResetRoutes } from "./user-password-reset-routes.js";
|
||||||
import { registerUserAdminRoutes } from "./user-admin-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
|
* @openapi
|
||||||
* /users/me:
|
* /users/me:
|
||||||
@@ -624,18 +601,19 @@ 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 shown = await shownIdentity(user);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
username: user.username,
|
username: shown.username,
|
||||||
is_admin: !!user.isAdmin,
|
is_admin: shown.isAdmin,
|
||||||
is_external: isExternalAccount(user),
|
is_external: isExternalAccount(user),
|
||||||
is_dual_auth: isDualAuth,
|
is_dual_auth: isDualAuth,
|
||||||
// Any second factor; the name is what 2.8 clients read.
|
// Any second factor; the name is what 2.8 clients read.
|
||||||
totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor(
|
totp_enabled: await createCurrentUserAuthRepository().hasSecondFactor(
|
||||||
user.id,
|
user.id,
|
||||||
),
|
),
|
||||||
linked: await describeDesktopLink(user.id),
|
linked: shown.linked,
|
||||||
// "second-factor-setup" while the login policy wants a factor set up.
|
// "second-factor-setup" while the login policy wants a factor set up.
|
||||||
restriction: (req as AuthenticatedRequest).sessionRestriction ?? null,
|
restriction: (req as AuthenticatedRequest).sessionRestriction ?? null,
|
||||||
});
|
});
|
||||||
@@ -758,10 +736,11 @@ router.post("/internal/auto-session", async (req, res) => {
|
|||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
const existingPayload = await authManager.verifyJWTToken(existingToken);
|
const existingPayload = await authManager.verifyJWTToken(existingToken);
|
||||||
if (existingPayload?.userId === userRecord.id) {
|
if (existingPayload?.userId === userRecord.id) {
|
||||||
|
const shownNow = await shownIdentity(userRecord);
|
||||||
return res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
is_admin: !!userRecord.isAdmin,
|
is_admin: shownNow.isAdmin,
|
||||||
username: userRecord.username,
|
username: shownNow.username,
|
||||||
token: existingToken,
|
token: existingToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -773,10 +752,11 @@ router.post("/internal/auto-session", async (req, res) => {
|
|||||||
rememberMe: true,
|
rememberMe: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shown = await shownIdentity(userRecord);
|
||||||
const response = {
|
const response = {
|
||||||
success: true,
|
success: true,
|
||||||
is_admin: !!userRecord.isAdmin,
|
is_admin: shown.isAdmin,
|
||||||
username: userRecord.username,
|
username: shown.username,
|
||||||
token,
|
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,
|
Auth,
|
||||||
getStoredAuth,
|
getStoredAuth,
|
||||||
clearStoredAuth,
|
clearStoredAuth,
|
||||||
|
storeAuth,
|
||||||
markDesktopManualLogout,
|
markDesktopManualLogout,
|
||||||
clearDesktopManualLogout,
|
clearDesktopManualLogout,
|
||||||
} from "@/auth/Auth";
|
} from "@/auth/Auth";
|
||||||
@@ -195,7 +196,12 @@ function App() {
|
|||||||
if (phase !== "verifying") return;
|
if (phase !== "verifying") return;
|
||||||
appReadyPromise
|
appReadyPromise
|
||||||
.then(() => getUserInfo())
|
.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()) {
|
if (isElectron()) {
|
||||||
try {
|
try {
|
||||||
const token = await getCurrentToken();
|
const token = await getCurrentToken();
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ function hasDesktopManualLogout() {
|
|||||||
return localStorage.getItem(DESKTOP_MANUAL_LOGOUT_KEY) === "true";
|
return localStorage.getItem(DESKTOP_MANUAL_LOGOUT_KEY) === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
function storeAuth(username: string) {
|
export function storeAuth(username: string) {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
JSON.stringify({ loggedIn: true, username }),
|
JSON.stringify({ loggedIn: true, username }),
|
||||||
|
|||||||
Reference in new issue
Block a user