feat: Initial user switching mutations

This commit is contained in:
2025-09-15 12:34:59 -04:00
parent c53fb152d4
commit 98876f23b8
8 changed files with 277 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
export const useUserSwitching = () => {
const session = useUserSession();
const isSwitched = computed(() => Boolean(session.data.value?.switchedBy));
const switchTo = async (userId: string | number) => {
const response = await $fetch("/api/switch-to", {
method: "POST",
body: { userId },
});
if (response.success) {
await session.fetch();
return response;
}
throw new Error(response.message || "Switch failed");
};
const switchBack = async () => {
const response = await $fetch("/api/switch-back", {
method: "POST",
});
if (response.success) {
await session.fetch();
return response;
}
throw new Error(response.message || "Switch back failed");
};
return {
isSwitched,
switchTo,
switchBack,
};
};

View File

@@ -22,6 +22,9 @@ require_once __DIR__ . '/includes/taxonomies/resource-category.php';
// Forms // Forms
// GraphQL
require_once __DIR__ . '/includes/graphql/user-switching.php';
// Roles // Roles
// Sections // Sections

View File

@@ -0,0 +1,103 @@
<?php
// Register userSwitchTo mutation
add_action( 'graphql_register_types', 'ccat_graphql_register_user_switch_to' );
function ccat_graphql_register_user_switch_to() {
register_graphql_mutation(
'userSwitchTo',
array(
'inputFields' => array(
'userId' => array(
'type' => 'ID',
'description' => esc_html__( 'The ID of the user to switch to', 'ccat' ),
),
),
'outputFields' => array(
'authToken' => array(
'type' => 'String',
'description' => esc_html__( 'JWT Token for the target user', 'ccat' ),
),
'refreshToken' => array(
'type' => 'String',
'description' => esc_html__( 'JWT Refresh Token for the target user', 'ccat' ),
),
'user' => array(
'type' => 'User',
'description' => esc_html__( 'The target user object', 'ccat' ),
),
),
'mutateAndGetPayload' => 'ccat_graphql_switch_to_mutation',
)
);
}
// Callback for userSwitchTo mutation
function ccat_graphql_switch_to_mutation( $input ) {
if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
throw new \GraphQL\Error\UserError( esc_html__( 'Insufficient permissions', 'ccat' ) );
}
$user_id = absint( $input['userId'] );
$current_user_id = get_current_user_id();
if ( $user_id === $current_user_id ) {
throw new \GraphQL\Error\UserError( esc_html__( 'Cannot switch to yourself', 'ccat' ) );
}
$target_user = get_user_by( 'ID', $user_id );
if ( ! $target_user ) {
throw new \GraphQL\Error\UserError( esc_html__( 'User not found', 'ccat' ) );
}
$secret_key = defined( 'GRAPHQL_JWT_AUTH_SECRET_KEY' ) ? GRAPHQL_JWT_AUTH_SECRET_KEY : wp_salt();
$issued_at = time();
$expire = $issued_at + ( DAY_IN_SECONDS * 7 );
$token_data = array(
'iss' => get_bloginfo( 'url' ),
'iat' => $issued_at,
'nbf' => $issued_at,
'exp' => $expire,
'data' => array(
'user' => array(
'id' => $target_user->ID,
),
'switched_by' => $current_user_id,
),
);
$auth_token = \Firebase\JWT\JWT::encode( $token_data, $secret_key, 'HS256' );
$refresh_token_data = array(
'iss' => get_bloginfo( 'url' ),
'iat' => $issued_at,
'nbf' => $issued_at,
'exp' => $issued_at + ( DAY_IN_SECONDS * 30 ),
'data' => array(
'user' => array( 'id' => $target_user->ID ),
'switched_by' => $current_user_id,
),
);
$refresh_token = \Firebase\JWT\JWT::encode( $refresh_token_data, $secret_key, 'HS256' );
return array(
'authToken' => $auth_token,
'refreshToken' => $refresh_token,
'user' => \WPGraphQL::get_app_context()->get_loader( 'user' )->load_deferred( $target_user->ID ),
);
}
// Register userSwitchBack mutation
add_action( 'graphql_register_types', 'ccat_graphql_register_user_switch_back' );
function ccat_graphql_register_user_switch_back() {
register_graphql_mutation(
'userSwitchBack',
array(
'inputFields' => array(),
'outputFields' => array(
'success' => array(
'type' => 'Boolean',
'description' => esc_html__( 'Whether switching back was successful', 'ccat' ),
),
),
'mutateAndGetPayload' => 'ccat_graphql_switch_back_mutation',
)
);
}
// Callback for userSwitchBack mutation
function ccat_graphql_switch_back_mutation() {
return array( 'success' => true );
}

View File

@@ -0,0 +1,19 @@
import { defineEventHandler } from "h3";
export default defineEventHandler(async (event) => {
try {
const response = await useGraphqlMutation("userSwitchBack");
if (response.errors?.length) {
throw new Error(response.errors[0]?.message || "Switch back failed");
}
await clearUserSession(event);
return { success: true };
}
catch (error) {
const message = error instanceof Error ? error.message : "Switch back failed";
return { success: false, message };
}
});

View File

@@ -0,0 +1,33 @@
import { defineEventHandler, readBody } from "h3";
export default defineEventHandler(async (event) => {
const { userId } = await readBody(event);
try {
const currentSession = await getUserSession(event);
if (!currentSession?.user) {
throw new Error("Authentication required");
}
const response = await useGraphqlMutation("userSwitchTo", { userId });
if (response.errors?.length) {
throw new Error(response.errors[0]?.message || "Switch failed");
}
const { authToken, refreshToken, user } = response.data.userSwitchTo;
await setUserSession(event, {
user,
secure: { authToken, refreshToken },
loggedInAt: new Date().toISOString(),
switchedBy: currentSession.user.id,
});
return { success: true };
}
catch (error) {
const message = error instanceof Error ? error.message : "Switch failed";
return { success: false, message };
}
});

View File

@@ -17146,6 +17146,18 @@ type RootMutation {
"""Input for the updateUser mutation""" """Input for the updateUser mutation"""
input: UpdateUserInput! input: UpdateUserInput!
): UpdateUserPayload ): UpdateUserPayload
"""The userSwitchBack mutation"""
userSwitchBack(
"""Input for the userSwitchBack mutation"""
input: UserSwitchBackInput!
): UserSwitchBackPayload
"""The userSwitchTo mutation"""
userSwitchTo(
"""Input for the userSwitchTo mutation"""
input: UserSwitchToInput!
): UserSwitchToPayload
} }
"""The root entry point into the Graph""" """The root entry point into the Graph"""
@@ -24625,6 +24637,53 @@ enum UserRoleEnum {
SUBSCRIBER SUBSCRIBER
} }
"""Input for the userSwitchBack mutation."""
input UserSwitchBackInput {
"""
This is an ID that can be passed to a mutation by the client to track the progress of mutations and catch possible duplicate mutation submissions.
"""
clientMutationId: String
}
"""The payload for the userSwitchBack mutation."""
type UserSwitchBackPayload {
"""
If a &#039;clientMutationId&#039; input is provided to the mutation, it will be returned as output on the mutation. This ID can be used by the client to track the progress of mutations and catch possible duplicate mutation submissions.
"""
clientMutationId: String
"""Whether switching back was successful"""
success: Boolean
}
"""Input for the userSwitchTo mutation."""
input UserSwitchToInput {
"""
This is an ID that can be passed to a mutation by the client to track the progress of mutations and catch possible duplicate mutation submissions.
"""
clientMutationId: String
"""The ID of the user to switch to"""
userId: ID
}
"""The payload for the userSwitchTo mutation."""
type UserSwitchToPayload {
"""JWT Token for the target user"""
authToken: String
"""
If a &#039;clientMutationId&#039; input is provided to the mutation, it will be returned as output on the mutation. This ID can be used by the client to track the progress of mutations and catch possible duplicate mutation submissions.
"""
clientMutationId: String
"""JWT Refresh Token for the target user"""
refreshToken: String
"""The target user object"""
user: User
}
"""Connection between the User type and the Comment type""" """Connection between the User type and the Comment type"""
type UserToCommentConnection implements CommentConnection & Connection { type UserToCommentConnection implements CommentConnection & Connection {
"""Edges for the UserToCommentConnection connection""" """Edges for the UserToCommentConnection connection"""

View File

@@ -0,0 +1,5 @@
mutation userSwitchBack {
userSwitchBack(input: {}) {
success
}
}

View File

@@ -0,0 +1,17 @@
mutation userSwitchTo($userId: ID!) {
userSwitchTo(input: { userId: $userId }) {
authToken
refreshToken
user {
id
databaseId
username
email
firstName
lastName
avatar {
url
}
}
}
}