mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-10 07:58:15 -05:00
Merge pull request #3489 from Infisical/feat/add-user-get-token-and-revamp-session-management
feat: add user get token CLI and revamp session management
This commit is contained in:
@@ -252,6 +252,31 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/me/sessions/:sessionId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
sessionId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.authToken.revokeMySessionById(req.permission.id, req.params.sessionId);
|
||||
return {
|
||||
message: "Successfully revoked session"
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/me",
|
||||
|
||||
@@ -47,7 +47,10 @@ export const tokenDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findTokenSessions = async (filter: Partial<TAuthTokenSessions>, tx?: Knex) => {
|
||||
try {
|
||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession).where(filter);
|
||||
const sessions = await (tx || db.replicaNode())(TableName.AuthTokenSession)
|
||||
.where(filter)
|
||||
.orderBy("lastUsed", "desc");
|
||||
|
||||
return sessions;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ name: "Find all token session", error });
|
||||
|
||||
@@ -151,6 +151,9 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
|
||||
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||
|
||||
const revokeMySessionById = async (userId: string, sessionId: string) =>
|
||||
tokenDAL.deleteTokenSession({ userId, id: sessionId });
|
||||
|
||||
const validateRefreshToken = async (refreshToken?: string) => {
|
||||
const appCfg = getConfig();
|
||||
if (!refreshToken)
|
||||
@@ -223,6 +226,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
clearTokenSessionById,
|
||||
getTokenSessionByUser,
|
||||
revokeAllMySessions,
|
||||
revokeMySessionById,
|
||||
validateRefreshToken,
|
||||
fnValidateJwtIdentity,
|
||||
getUserTokenSessionById
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
@@ -85,6 +88,57 @@ var switchCmd = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var userGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Used to get properties of an Infisical profile",
|
||||
DisableFlagsInUseLine: true,
|
||||
Example: "infisical user get",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
var userGetTokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Used to get the access token of an Infisical user",
|
||||
DisableFlagsInUseLine: true,
|
||||
Example: "infisical user get token",
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRun: func(cmd *cobra.Command, args []string) {
|
||||
util.RequireLogin()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||
if loggedInUserDetails.LoginExpired {
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
if err != nil {
|
||||
util.HandleError(err, "[infisical user get token]: Unable to get logged in user token")
|
||||
}
|
||||
|
||||
tokenParts := strings.Split(loggedInUserDetails.UserCredentials.JTWToken, ".")
|
||||
if len(tokenParts) != 3 {
|
||||
util.HandleError(errors.New("invalid token format"), "[infisical user get token]: Invalid token format")
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(tokenParts[1])
|
||||
if err != nil {
|
||||
util.HandleError(err, "[infisical user get token]: Unable to decode token payload")
|
||||
}
|
||||
|
||||
var tokenPayload struct {
|
||||
TokenVersionId string `json:"tokenVersionId"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &tokenPayload); err != nil {
|
||||
util.HandleError(err, "[infisical user get token]: Unable to parse token payload")
|
||||
}
|
||||
|
||||
fmt.Println("Session ID:", tokenPayload.TokenVersionId)
|
||||
fmt.Println("Token:", loggedInUserDetails.UserCredentials.JTWToken)
|
||||
},
|
||||
}
|
||||
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update",
|
||||
Short: "Used to update properties of an Infisical profile",
|
||||
@@ -185,6 +239,8 @@ var domainCmd = &cobra.Command{
|
||||
func init() {
|
||||
updateCmd.AddCommand(domainCmd)
|
||||
userCmd.AddCommand(updateCmd)
|
||||
userGetCmd.AddCommand(userGetTokenCmd)
|
||||
userCmd.AddCommand(userGetCmd)
|
||||
userCmd.AddCommand(switchCmd)
|
||||
rootCmd.AddCommand(userCmd)
|
||||
}
|
||||
|
||||
@@ -8,22 +8,46 @@ infisical user
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
This command allows you to manage the current logged in users on the CLI
|
||||
|
||||
### Sub-commands
|
||||
<Accordion title="infisical user switch" defaultOpen="true">
|
||||
Use this command to switch between profiles that are currently logged into the CLI
|
||||
### Sub-commands
|
||||
|
||||
<Accordion title="infisical user switch" defaultOpen="true">
|
||||
Use this command to switch between profiles that are currently logged into the CLI
|
||||
|
||||
```bash
|
||||
infisical user switch
|
||||
```
|
||||
|
||||
```bash
|
||||
infisical user switch
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical user update domain">
|
||||
With this command, you can modify the backend API that is utilized for all requests associated with a specific profile.
|
||||
For instance, you have the option to point the profile to use either the Infisical Cloud or your own self-hosted Infisical instance.
|
||||
|
||||
```bash
|
||||
infisical user update domain
|
||||
```bash
|
||||
infisical user update domain
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="infisical user get token">
|
||||
Use this command to get your current Infisical access token and session information. This command requires you to be logged in.
|
||||
|
||||
The command will display:
|
||||
|
||||
- Your session ID
|
||||
- Your full JWT access token
|
||||
|
||||
```bash
|
||||
infisical user get token
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
||||
```bash
|
||||
Session ID: abc123-xyz-456
|
||||
Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
useAddUserToWsE2EE,
|
||||
useAddUserToWsNonE2EE,
|
||||
useRevokeMySessionById,
|
||||
useSendEmailVerificationCode,
|
||||
useVerifyEmailVerificationCode
|
||||
} from "./mutation";
|
||||
|
||||
@@ -171,3 +171,16 @@ export const useResendOrgMemberInvitation = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useRevokeMySessionById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (sessionId: string) => {
|
||||
const { data } = await apiRequest.delete(`/api/v2/users/me/sessions/${sessionId}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({ queryKey: userKeys.mySessions });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,3 +11,65 @@ export const formatReservedPaths = (secretPath: string) => {
|
||||
export const camelCaseToSpaces = (input: string) => {
|
||||
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||
};
|
||||
|
||||
export const formatSessionUserAgent = (userAgent: string) => {
|
||||
const result = {
|
||||
os: "Unknown",
|
||||
browser: "Unknown",
|
||||
device: "Desktop"
|
||||
};
|
||||
|
||||
// Operating System detection
|
||||
if (userAgent.includes("Windows")) {
|
||||
result.os = "Windows";
|
||||
} else if (
|
||||
userAgent.includes("Mac OS") ||
|
||||
userAgent.includes("Macintosh") ||
|
||||
userAgent.includes("macOS")
|
||||
) {
|
||||
result.os = "macOS";
|
||||
} else if (userAgent.includes("Linux") && !userAgent.includes("Android")) {
|
||||
result.os = "Linux";
|
||||
} else if (userAgent.includes("Android")) {
|
||||
result.os = "Android";
|
||||
result.device = "Mobile";
|
||||
} else if (
|
||||
userAgent.includes("iOS") ||
|
||||
userAgent.includes("iPhone") ||
|
||||
userAgent.includes("iPad")
|
||||
) {
|
||||
result.os = "iOS";
|
||||
result.device = userAgent.includes("iPad") ? "Tablet" : "Mobile";
|
||||
}
|
||||
|
||||
// Browser detection
|
||||
if (userAgent.includes("Firefox/")) {
|
||||
result.browser = "Firefox";
|
||||
} else if (userAgent.includes("Edge/") || userAgent.includes("Edg/")) {
|
||||
result.browser = "Edge";
|
||||
} else if (userAgent.includes("Brave/") || userAgent.includes("Brave ")) {
|
||||
result.browser = "Brave";
|
||||
} else if (
|
||||
userAgent.includes("Chrome/") &&
|
||||
!userAgent.includes("Chromium/") &&
|
||||
!userAgent.includes("Edg/")
|
||||
) {
|
||||
result.browser = "Chrome";
|
||||
} else if (
|
||||
userAgent.includes("Safari/") &&
|
||||
!userAgent.includes("Chrome/") &&
|
||||
!userAgent.includes("Chromium/")
|
||||
) {
|
||||
result.browser = "Safari";
|
||||
} else if (userAgent.includes("Opera/") || userAgent.includes("OPR/")) {
|
||||
result.browser = "Opera";
|
||||
} else if (userAgent.includes("Trident/") || userAgent.includes("MSIE")) {
|
||||
result.browser = "Internet Explorer";
|
||||
}
|
||||
|
||||
if (userAgent.toLowerCase() === "cli") {
|
||||
result.browser = "CLI";
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
Table,
|
||||
TableContainer,
|
||||
@@ -9,59 +11,131 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useGetMySessions } from "@app/hooks/api";
|
||||
import { Button } from "@app/components/v2/Button";
|
||||
import { useGetMySessions, useRevokeMySessionById } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import { timeAgo } from "@app/lib/fn/date";
|
||||
import { formatSessionUserAgent } from "@app/lib/fn/string";
|
||||
|
||||
const formatLocalDateTime = (date: Date): string => {
|
||||
return date.toLocaleString(undefined, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
});
|
||||
};
|
||||
|
||||
export const SessionsTable = () => {
|
||||
const { data, isPending } = useGetMySessions();
|
||||
const { mutateAsync: revokeMySessionById } = useRevokeMySessionById();
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"deleteSession"
|
||||
] as const);
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const handleSignOut = async (sessionId: string) => {
|
||||
try {
|
||||
await revokeMySessionById(sessionId);
|
||||
createNotification({
|
||||
text: "Session revoked successfully",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to revoke session",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
const formattedDate = `${day}/${month}/${year}`;
|
||||
|
||||
return formattedDate;
|
||||
handlePopUpClose("deleteSession");
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Created</Th>
|
||||
<Th>Last active</Th>
|
||||
<Th>IP address</Th>
|
||||
<Th>Device</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="sesssions" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
|
||||
return (
|
||||
<Tr className="h-10" key={`session-${id}`}>
|
||||
<Td>{formatDate(createdAt)}</Td>
|
||||
<Td>{formatDate(lastUsed)}</Td>
|
||||
<Td>{ip}</Td>
|
||||
<Td>{userAgent}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSession.isOpen}
|
||||
title="Are you sure want to sign out of this session?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSession", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() =>
|
||||
handleSignOut((popUp?.deleteSession?.data as { sessionId: string })?.sessionId)
|
||||
}
|
||||
/>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState title="No sessions on file" icon={faServer} />
|
||||
</Td>
|
||||
<Th>IP & Session ID</Th>
|
||||
<Th>OS & Browser</Th>
|
||||
<Th>Last accessed</Th>
|
||||
<Th>Manage</Th>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={4} innerKey="sessions" />}
|
||||
{!isPending &&
|
||||
data &&
|
||||
data.length > 0 &&
|
||||
data.map(({ id, createdAt, lastUsed, ip, userAgent }) => {
|
||||
const { os, browser } = formatSessionUserAgent(userAgent);
|
||||
const lastUsedDate = new Date(lastUsed);
|
||||
const createdAtDate = new Date(createdAt);
|
||||
|
||||
return (
|
||||
<Tr className="h-20" key={`session-${id}`}>
|
||||
<Td>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{ip}</span>
|
||||
<span className="text-sm text-gray-500">ID: {id}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{os}</span>
|
||||
<span className="text-sm text-gray-500">{browser}</span>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-col">
|
||||
<Tooltip content={formatLocalDateTime(lastUsedDate)}>
|
||||
<span className="font-medium">{timeAgo(lastUsedDate, new Date())}</span>
|
||||
</Tooltip>
|
||||
<Tooltip content={formatLocalDateTime(createdAtDate)}>
|
||||
<span className="text-sm text-gray-500">
|
||||
Created {timeAgo(createdAtDate, new Date())}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
onClick={() => handlePopUpOpen("deleteSession", { sessionId: id })}
|
||||
>
|
||||
Sign out
|
||||
</Button>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isPending && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState title="No sessions on file" icon={faServer} />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user