From 8dc09ea4e7cbe1d66317a489befe82cc4b46627a Mon Sep 17 00:00:00 2001 From: Meng Zhang Date: Sat, 9 Dec 2023 18:12:54 +0800 Subject: [PATCH] refactor: restructure JwtPayload and MeQuery (#994) * extract schema::User as format for MeQuery * refactor: restructure JWTPayload * feat: update frontend to adapt JWT token format change * delete generated files --- ee/tabby-ui/lib/gql/generates/gql.ts | 87 ---- ee/tabby-ui/lib/gql/generates/graphql.ts | 511 --------------------- ee/tabby-ui/lib/tabby/auth.tsx | 8 +- ee/tabby-webserver/graphql/schema.graphql | 29 +- ee/tabby-webserver/src/schema/auth.rs | 69 +-- ee/tabby-webserver/src/schema/mod.rs | 35 +- ee/tabby-webserver/src/service/auth.rs | 40 +- ee/tabby-webserver/src/service/db/users.rs | 11 + 8 files changed, 104 insertions(+), 686 deletions(-) delete mode 100644 ee/tabby-ui/lib/gql/generates/gql.ts delete mode 100644 ee/tabby-ui/lib/gql/generates/graphql.ts diff --git a/ee/tabby-ui/lib/gql/generates/gql.ts b/ee/tabby-ui/lib/gql/generates/gql.ts deleted file mode 100644 index b22704e..0000000 --- a/ee/tabby-ui/lib/gql/generates/gql.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' - -import * as types from './graphql' - -/** - * Map of all GraphQL operations in the project. - * - * This map has several performance disadvantages: - * 1. It is not tree-shakeable, so it will include all operations in the project. - * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle. - * 3. It does not support dead code elimination, so it will add unused operations. - * - * Therefore it is highly recommended to use the babel or swc plugin for production. - */ -const documents = { - '\n query GetRegistrationToken {\n registrationToken\n }\n': - types.GetRegistrationTokenDocument, - '\n mutation tokenAuth($email: String!, $password: String!) {\n tokenAuth(email: $email, password: $password) {\n accessToken\n refreshToken\n }\n }\n': - types.TokenAuthDocument, - '\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n': - types.RegisterDocument, - '\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n': - types.GetWorkersDocument, - '\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n': - types.RefreshTokenDocument, - '\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n': - types.GetIsAdminInitializedDocument -} - -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - * - * - * @example - * ```ts - * const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`); - * ``` - * - * The query argument is unknown! - * Please regenerate the types. - */ -export function graphql(source: string): unknown - -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: '\n query GetRegistrationToken {\n registrationToken\n }\n' -): (typeof documents)['\n query GetRegistrationToken {\n registrationToken\n }\n'] -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: '\n mutation tokenAuth($email: String!, $password: String!) {\n tokenAuth(email: $email, password: $password) {\n accessToken\n refreshToken\n }\n }\n' -): (typeof documents)['\n mutation tokenAuth($email: String!, $password: String!) {\n tokenAuth(email: $email, password: $password) {\n accessToken\n refreshToken\n }\n }\n'] -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: '\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n' -): (typeof documents)['\n mutation register(\n $email: String!\n $password1: String!\n $password2: String!\n $invitationCode: String\n ) {\n register(\n email: $email\n password1: $password1\n password2: $password2\n invitationCode: $invitationCode\n ) {\n accessToken\n refreshToken\n }\n }\n'] -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: '\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n' -): (typeof documents)['\n query GetWorkers {\n workers {\n kind\n name\n addr\n device\n arch\n cpuInfo\n cpuCount\n cudaDevices\n }\n }\n'] -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: '\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n' -): (typeof documents)['\n mutation refreshToken($refreshToken: String!) {\n refreshToken(refreshToken: $refreshToken) {\n accessToken\n refreshToken\n }\n }\n'] -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: '\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n' -): (typeof documents)['\n query GetIsAdminInitialized {\n isAdminInitialized\n }\n'] - -export function graphql(source: string) { - return (documents as any)[source] ?? {} -} - -export type DocumentType> = - TDocumentNode extends DocumentNode ? TType : never diff --git a/ee/tabby-ui/lib/gql/generates/graphql.ts b/ee/tabby-ui/lib/gql/generates/graphql.ts deleted file mode 100644 index 417e2ad..0000000 --- a/ee/tabby-ui/lib/gql/generates/graphql.ts +++ /dev/null @@ -1,511 +0,0 @@ -/* eslint-disable */ -import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core' - -export type Maybe = T | null -export type InputMaybe = Maybe -export type Exact = { - [K in keyof T]: T[K] -} -export type MakeOptional = Omit & { - [SubKey in K]?: Maybe -} -export type MakeMaybe = Omit & { - [SubKey in K]: Maybe -} -export type MakeEmpty< - T extends { [key: string]: unknown }, - K extends keyof T -> = { [_ in K]?: never } -export type Incremental = - | T - | { - [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never - } -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: { input: string; output: string } - String: { input: string; output: string } - Boolean: { input: boolean; output: boolean } - Int: { input: number; output: number } - Float: { input: number; output: number } -} - -export type Claims = { - __typename?: 'Claims' - exp: Scalars['Float']['output'] - iat: Scalars['Float']['output'] - user: UserInfo -} - -export type Invitation = { - __typename?: 'Invitation' - code: Scalars['String']['output'] - createdAt: Scalars['String']['output'] - email: Scalars['String']['output'] - id: Scalars['Int']['output'] -} - -export type Mutation = { - __typename?: 'Mutation' - createInvitation: Scalars['Int']['output'] - deleteInvitation: Scalars['Int']['output'] - refreshToken: RefreshTokenResponse - register: RegisterResponse - resetRegistrationToken: Scalars['String']['output'] - tokenAuth: TokenAuthResponse - verifyToken: VerifyTokenResponse -} - -export type MutationCreateInvitationArgs = { - email: Scalars['String']['input'] -} - -export type MutationDeleteInvitationArgs = { - id: Scalars['Int']['input'] -} - -export type MutationRefreshTokenArgs = { - refreshToken: Scalars['String']['input'] -} - -export type MutationRegisterArgs = { - email: Scalars['String']['input'] - invitationCode?: InputMaybe - password1: Scalars['String']['input'] - password2: Scalars['String']['input'] -} - -export type MutationTokenAuthArgs = { - email: Scalars['String']['input'] - password: Scalars['String']['input'] -} - -export type MutationVerifyTokenArgs = { - token: Scalars['String']['input'] -} - -export type Query = { - __typename?: 'Query' - invitations: Array - isAdminInitialized: Scalars['Boolean']['output'] - me: UserInfo - registrationToken: Scalars['String']['output'] - workers: Array -} - -export type RefreshTokenResponse = { - __typename?: 'RefreshTokenResponse' - accessToken: Scalars['String']['output'] - refreshExpiresAt: Scalars['Float']['output'] - refreshToken: Scalars['String']['output'] -} - -export type RegisterResponse = { - __typename?: 'RegisterResponse' - accessToken: Scalars['String']['output'] - refreshToken: Scalars['String']['output'] -} - -export type TokenAuthResponse = { - __typename?: 'TokenAuthResponse' - accessToken: Scalars['String']['output'] - refreshToken: Scalars['String']['output'] -} - -export type UserInfo = { - __typename?: 'UserInfo' - email: Scalars['String']['output'] - isAdmin: Scalars['Boolean']['output'] -} - -export type VerifyTokenResponse = { - __typename?: 'VerifyTokenResponse' - claims: Claims -} - -export type Worker = { - __typename?: 'Worker' - addr: Scalars['String']['output'] - arch: Scalars['String']['output'] - cpuCount: Scalars['Int']['output'] - cpuInfo: Scalars['String']['output'] - cudaDevices: Array - device: Scalars['String']['output'] - kind: WorkerKind - name: Scalars['String']['output'] -} - -export enum WorkerKind { - Chat = 'CHAT', - Completion = 'COMPLETION' -} - -export type GetRegistrationTokenQueryVariables = Exact<{ [key: string]: never }> - -export type GetRegistrationTokenQuery = { - __typename?: 'Query' - registrationToken: string -} - -export type TokenAuthMutationVariables = Exact<{ - email: Scalars['String']['input'] - password: Scalars['String']['input'] -}> - -export type TokenAuthMutation = { - __typename?: 'Mutation' - tokenAuth: { - __typename?: 'TokenAuthResponse' - accessToken: string - refreshToken: string - } -} - -export type RegisterMutationVariables = Exact<{ - email: Scalars['String']['input'] - password1: Scalars['String']['input'] - password2: Scalars['String']['input'] - invitationCode?: InputMaybe -}> - -export type RegisterMutation = { - __typename?: 'Mutation' - register: { - __typename?: 'RegisterResponse' - accessToken: string - refreshToken: string - } -} - -export type GetWorkersQueryVariables = Exact<{ [key: string]: never }> - -export type GetWorkersQuery = { - __typename?: 'Query' - workers: Array<{ - __typename?: 'Worker' - kind: WorkerKind - name: string - addr: string - device: string - arch: string - cpuInfo: string - cpuCount: number - cudaDevices: Array - }> -} - -export type RefreshTokenMutationVariables = Exact<{ - refreshToken: Scalars['String']['input'] -}> - -export type RefreshTokenMutation = { - __typename?: 'Mutation' - refreshToken: { - __typename?: 'RefreshTokenResponse' - accessToken: string - refreshToken: string - } -} - -export type GetIsAdminInitializedQueryVariables = Exact<{ - [key: string]: never -}> - -export type GetIsAdminInitializedQuery = { - __typename?: 'Query' - isAdminInitialized: boolean -} - -export const GetRegistrationTokenDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'GetRegistrationToken' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'registrationToken' } } - ] - } - } - ] -} as unknown as DocumentNode< - GetRegistrationTokenQuery, - GetRegistrationTokenQueryVariables -> -export const TokenAuthDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'mutation', - name: { kind: 'Name', value: 'tokenAuth' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'email' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'password' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'tokenAuth' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'email' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'email' } - } - }, - { - kind: 'Argument', - name: { kind: 'Name', value: 'password' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'password' } - } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'accessToken' } }, - { kind: 'Field', name: { kind: 'Name', value: 'refreshToken' } } - ] - } - } - ] - } - } - ] -} as unknown as DocumentNode -export const RegisterDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'mutation', - name: { kind: 'Name', value: 'register' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'email' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'password1' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'password2' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - }, - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'invitationCode' } - }, - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'register' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'email' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'email' } - } - }, - { - kind: 'Argument', - name: { kind: 'Name', value: 'password1' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'password1' } - } - }, - { - kind: 'Argument', - name: { kind: 'Name', value: 'password2' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'password2' } - } - }, - { - kind: 'Argument', - name: { kind: 'Name', value: 'invitationCode' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'invitationCode' } - } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'accessToken' } }, - { kind: 'Field', name: { kind: 'Name', value: 'refreshToken' } } - ] - } - } - ] - } - } - ] -} as unknown as DocumentNode -export const GetWorkersDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'GetWorkers' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'workers' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'kind' } }, - { kind: 'Field', name: { kind: 'Name', value: 'name' } }, - { kind: 'Field', name: { kind: 'Name', value: 'addr' } }, - { kind: 'Field', name: { kind: 'Name', value: 'device' } }, - { kind: 'Field', name: { kind: 'Name', value: 'arch' } }, - { kind: 'Field', name: { kind: 'Name', value: 'cpuInfo' } }, - { kind: 'Field', name: { kind: 'Name', value: 'cpuCount' } }, - { kind: 'Field', name: { kind: 'Name', value: 'cudaDevices' } } - ] - } - } - ] - } - } - ] -} as unknown as DocumentNode -export const RefreshTokenDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'mutation', - name: { kind: 'Name', value: 'refreshToken' }, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: { - kind: 'Variable', - name: { kind: 'Name', value: 'refreshToken' } - }, - type: { - kind: 'NonNullType', - type: { kind: 'NamedType', name: { kind: 'Name', value: 'String' } } - } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: { kind: 'Name', value: 'refreshToken' }, - arguments: [ - { - kind: 'Argument', - name: { kind: 'Name', value: 'refreshToken' }, - value: { - kind: 'Variable', - name: { kind: 'Name', value: 'refreshToken' } - } - } - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'accessToken' } }, - { kind: 'Field', name: { kind: 'Name', value: 'refreshToken' } } - ] - } - } - ] - } - } - ] -} as unknown as DocumentNode< - RefreshTokenMutation, - RefreshTokenMutationVariables -> -export const GetIsAdminInitializedDocument = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: { kind: 'Name', value: 'GetIsAdminInitialized' }, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { kind: 'Field', name: { kind: 'Name', value: 'isAdminInitialized' } } - ] - } - } - ] -} as unknown as DocumentNode< - GetIsAdminInitializedQuery, - GetIsAdminInitializedQueryVariables -> diff --git a/ee/tabby-ui/lib/tabby/auth.tsx b/ee/tabby-ui/lib/tabby/auth.tsx index 30083de..642cea0 100644 --- a/ee/tabby-ui/lib/tabby/auth.tsx +++ b/ee/tabby-ui/lib/tabby/auth.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { useRouter } from 'next/navigation' -import { jwtDecode } from 'jwt-decode' +import { jwtDecode, JwtPayload } from 'jwt-decode' import { graphql } from '@/lib/gql/generates' import useInterval from '@/lib/hooks/use-interval' @@ -226,13 +226,13 @@ type Session = function useSession(): Session { const { authState } = useAuthStore() if (authState?.status == 'authenticated') { - const { user } = jwtDecode<{ user: { email: string; is_admin: boolean } }>( + const { sub, is_admin } = jwtDecode( authState.data.accessToken ) return { data: { - email: user.email, - isAdmin: user.is_admin, + email: sub!, + isAdmin: is_admin, accessToken: authState.data.accessToken }, status: authState.status diff --git a/ee/tabby-webserver/graphql/schema.graphql b/ee/tabby-webserver/graphql/schema.graphql index 366f527..d73974a 100644 --- a/ee/tabby-webserver/graphql/schema.graphql +++ b/ee/tabby-webserver/graphql/schema.graphql @@ -18,19 +18,22 @@ type Mutation { deleteInvitation(id: Int!): Int! } +"DateTime" +scalar DateTimeUtc + type VerifyTokenResponse { - claims: Claims! + claims: JWTPayload! } -type UserInfo { - email: String! - isAdmin: Boolean! -} - -type Claims { +type JWTPayload { + "Expiration time (as UTC timestamp)" exp: Float! + "Issued at (as UTC timestamp)" iat: Float! - user: UserInfo! + "User email address" + sub: String! + "Whether the user is admin." + isAdmin: Boolean! } type Query { @@ -38,7 +41,7 @@ type Query { registrationToken: String! isAdminInitialized: Boolean! invitations: [Invitation!]! - me: UserInfo! + me: User! } type Invitation { @@ -48,6 +51,12 @@ type Invitation { createdAt: String! } +type User { + email: String! + isAdmin: Boolean! + authToken: String! +} + type Worker { kind: WorkerKind! name: String! @@ -67,7 +76,7 @@ type TokenAuthResponse { type RefreshTokenResponse { accessToken: String! refreshToken: String! - refreshExpiresAt: Float! + refreshExpiresAt: DateTimeUtc! } schema { diff --git a/ee/tabby-webserver/src/schema/auth.rs b/ee/tabby-webserver/src/schema/auth.rs index a6dc98e..648174f 100644 --- a/ee/tabby-webserver/src/schema/auth.rs +++ b/ee/tabby-webserver/src/schema/auth.rs @@ -12,7 +12,7 @@ use tracing::{error, warn}; use uuid::Uuid; use validator::ValidationErrors; -use super::from_validation_errors; +use super::{from_validation_errors, User}; lazy_static! { static ref JWT_TOKEN_SECRET: String = jwt_token_secret(); @@ -26,15 +26,15 @@ lazy_static! { static ref JWT_DEFAULT_EXP: u64 = 30 * 60; // 30 minutes } -pub fn generate_jwt(claims: Claims) -> jwt::errors::Result { +pub fn generate_jwt(claims: JWTPayload) -> jwt::errors::Result { let header = jwt::Header::default(); let token = jwt::encode(&header, &claims, &JWT_ENCODING_KEY)?; Ok(token) } -pub fn validate_jwt(token: &str) -> jwt::errors::Result { +pub fn validate_jwt(token: &str) -> jwt::errors::Result { let validation = jwt::Validation::default(); - let data = jwt::decode::(token, &JWT_DECODING_KEY, &validation)?; + let data = jwt::decode::(token, &JWT_DECODING_KEY, &validation)?; Ok(data.claims) } @@ -202,58 +202,40 @@ impl RefreshTokenResponse { #[derive(Debug, GraphQLObject)] pub struct VerifyTokenResponse { - claims: Claims, + claims: JWTPayload, } impl VerifyTokenResponse { - pub fn new(claims: Claims) -> Self { + pub fn new(claims: JWTPayload) -> Self { Self { claims } } } -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, GraphQLObject)] -pub struct UserInfo { - email: String, - is_admin: bool, -} - -impl UserInfo { - pub fn new(email: String, is_admin: bool) -> Self { - Self { email, is_admin } - } - - pub fn is_admin(&self) -> bool { - self.is_admin - } - - pub fn email(&self) -> &str { - &self.email - } -} - #[derive(Debug, Default, Serialize, Deserialize, GraphQLObject)] -pub struct Claims { - // Required. Expiration time (as UTC timestamp) +pub struct JWTPayload { + /// Expiration time (as UTC timestamp) exp: f64, - // Optional. Issued at (as UTC timestamp) + + /// Issued at (as UTC timestamp) iat: f64, - // Customized. user info - user: UserInfo, + + /// User email address + pub sub: String, + + /// Whether the user is admin. + pub is_admin: bool, } -impl Claims { - pub fn new(user: UserInfo) -> Self { +impl JWTPayload { + pub fn new(email: String, is_admin: bool) -> Self { let now = jwt::get_current_timestamp(); Self { iat: now as f64, exp: (now + *JWT_DEFAULT_EXP) as f64, - user, + sub: email, + is_admin, } } - - pub fn user_info(&self) -> &UserInfo { - &self.user - } } #[derive(Debug, Default, Serialize, Deserialize, GraphQLObject)] @@ -287,6 +269,7 @@ pub trait AuthenticationService: Send + Sync { ) -> std::result::Result; async fn verify_access_token(&self, access_token: &str) -> Result; async fn is_admin_initialized(&self) -> Result; + async fn get_user_by_email(&self, email: &str) -> Result; async fn create_invitation(&self, email: String) -> Result; async fn list_invitations(&self) -> Result>; @@ -298,7 +281,7 @@ mod tests { use super::*; #[test] fn test_generate_jwt() { - let claims = Claims::new(UserInfo::new("test".to_string(), false)); + let claims = JWTPayload::new("test".to_string(), false); let token = generate_jwt(claims).unwrap(); assert!(!token.is_empty()) @@ -306,13 +289,11 @@ mod tests { #[test] fn test_validate_jwt() { - let claims = Claims::new(UserInfo::new("test".to_string(), false)); + let claims = JWTPayload::new("test".to_string(), false); let token = generate_jwt(claims).unwrap(); let claims = validate_jwt(&token).unwrap(); - assert_eq!( - claims.user_info(), - &UserInfo::new("test".to_string(), false) - ); + assert_eq!(claims.sub, "test"); + assert!(!claims.is_admin); } #[test] diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs index eaad8e9..e1ceec0 100644 --- a/ee/tabby-webserver/src/schema/mod.rs +++ b/ee/tabby-webserver/src/schema/mod.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use auth::AuthenticationService; use juniper::{ - graphql_object, graphql_value, EmptySubscription, FieldError, IntoFieldError, Object, RootNode, - ScalarValue, Value, + graphql_object, graphql_value, EmptySubscription, FieldError, GraphQLObject, IntoFieldError, + Object, RootNode, ScalarValue, Value, }; use juniper_axum::FromAuth; use tabby_common::api::{code::CodeSearch, event::RawEventLogger}; @@ -18,7 +18,7 @@ use self::{ }; use crate::schema::{ auth::{ - RefreshTokenError, RefreshTokenResponse, RegisterResponse, TokenAuthResponse, UserInfo, + RefreshTokenError, RefreshTokenResponse, RegisterResponse, TokenAuthResponse, VerifyTokenResponse, }, worker::Worker, @@ -32,7 +32,7 @@ pub trait ServiceLocator: Send + Sync { } pub struct Context { - claims: Option, + claims: Option, locator: Arc, } @@ -73,7 +73,7 @@ pub struct Query; impl Query { async fn workers(ctx: &Context) -> Result> { if let Some(claims) = &ctx.claims { - if claims.user_info().is_admin() { + if claims.is_admin { let workers = ctx.locator.worker().list_workers().await; return Ok(workers); } @@ -85,7 +85,7 @@ impl Query { async fn registration_token(ctx: &Context) -> Result { if let Some(claims) = &ctx.claims { - if claims.user_info().is_admin() { + if claims.is_admin { let token = ctx.locator.worker().read_registration_token().await?; return Ok(token); } @@ -101,7 +101,7 @@ impl Query { async fn invitations(ctx: &Context) -> Result> { if let Some(claims) = &ctx.claims { - if claims.user_info().is_admin() { + if claims.is_admin { return Ok(ctx.locator.auth().list_invitations().await?); } } @@ -110,14 +110,23 @@ impl Query { )) } - async fn me(ctx: &Context) -> Result { + async fn me(ctx: &Context) -> Result { if let Some(claims) = &ctx.claims { - return Ok(claims.user_info().to_owned()); + let user = ctx.locator.auth().get_user_by_email(&claims.sub).await?; + Ok(user) + } else { + Err(CoreError::Unauthorized("Not logged in")) } - Err(CoreError::Unauthorized("Not logged in")) } } +#[derive(Debug, GraphQLObject)] +pub struct User { + pub email: String, + pub is_admin: bool, + pub auth_token: String, +} + #[derive(Default)] pub struct Mutation; @@ -125,7 +134,7 @@ pub struct Mutation; impl Mutation { async fn reset_registration_token(ctx: &Context) -> Result { if let Some(claims) = &ctx.claims { - if claims.user_info().is_admin() { + if claims.is_admin { let reg_token = ctx.locator.worker().reset_registration_token().await?; return Ok(reg_token); } @@ -169,7 +178,7 @@ impl Mutation { async fn create_invitation(ctx: &Context, email: String) -> Result { if let Some(claims) = &ctx.claims { - if claims.user_info().is_admin() { + if claims.is_admin { return Ok(ctx.locator.auth().create_invitation(email).await?); } } @@ -180,7 +189,7 @@ impl Mutation { async fn delete_invitation(ctx: &Context, id: i32) -> Result { if let Some(claims) = &ctx.claims { - if claims.user_info().is_admin() { + if claims.is_admin { return Ok(ctx.locator.auth().delete_invitation(id).await?); } } diff --git a/ee/tabby-webserver/src/service/auth.rs b/ee/tabby-webserver/src/service/auth.rs index 0b5345c..44a94f1 100644 --- a/ee/tabby-webserver/src/service/auth.rs +++ b/ee/tabby-webserver/src/service/auth.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use argon2::{ password_hash, password_hash::{rand_core::OsRng, SaltString}, @@ -8,10 +8,13 @@ use async_trait::async_trait; use validator::Validate; use super::db::DbConn; -use crate::schema::auth::{ - generate_jwt, generate_refresh_token, validate_jwt, AuthenticationService, Claims, Invitation, - RefreshTokenError, RefreshTokenResponse, RegisterError, RegisterResponse, TokenAuthError, - TokenAuthResponse, UserInfo, VerifyTokenResponse, +use crate::schema::{ + auth::{ + generate_jwt, generate_refresh_token, validate_jwt, AuthenticationService, Invitation, + JWTPayload, RefreshTokenError, RefreshTokenResponse, RegisterError, RegisterResponse, + TokenAuthError, TokenAuthResponse, VerifyTokenResponse, + }, + User, }; /// Input parameters for register mutation @@ -149,10 +152,8 @@ impl AuthenticationService for DbConn { let refresh_token = generate_refresh_token(); self.create_refresh_token(id, &refresh_token).await?; - let Ok(access_token) = generate_jwt(Claims::new(UserInfo::new( - user.email.clone(), - user.is_admin, - ))) else { + let Ok(access_token) = generate_jwt(JWTPayload::new(user.email.clone(), user.is_admin)) + else { return Err(RegisterError::Unknown); }; @@ -179,10 +180,8 @@ impl AuthenticationService for DbConn { let refresh_token = generate_refresh_token(); self.create_refresh_token(user.id, &refresh_token).await?; - let Ok(access_token) = generate_jwt(Claims::new(UserInfo::new( - user.email.clone(), - user.is_admin, - ))) else { + let Ok(access_token) = generate_jwt(JWTPayload::new(user.email.clone(), user.is_admin)) + else { return Err(TokenAuthError::Unknown); }; @@ -208,10 +207,8 @@ impl AuthenticationService for DbConn { self.replace_refresh_token(&token, &new_token).await?; // refresh token update is done, generate new access token based on user info - let Ok(access_token) = generate_jwt(Claims::new(UserInfo::new( - user.email.clone(), - user.is_admin, - ))) else { + let Ok(access_token) = generate_jwt(JWTPayload::new(user.email.clone(), user.is_admin)) + else { return Err(RefreshTokenError::Unknown); }; @@ -231,6 +228,15 @@ impl AuthenticationService for DbConn { Ok(!admin.is_empty()) } + async fn get_user_by_email(&self, email: &str) -> Result { + let user = self.get_user_by_email(email).await?; + if let Some(user) = user { + Ok(user.into()) + } else { + Err(anyhow!("User not found {}", email)) + } + } + async fn create_invitation(&self, email: String) -> Result { self.create_invitation(email).await } diff --git a/ee/tabby-webserver/src/service/db/users.rs b/ee/tabby-webserver/src/service/db/users.rs index 245c7af..a4afa86 100644 --- a/ee/tabby-webserver/src/service/db/users.rs +++ b/ee/tabby-webserver/src/service/db/users.rs @@ -6,6 +6,7 @@ use rusqlite::{params, OptionalExtension, Row}; use uuid::Uuid; use super::DbConn; +use crate::schema; #[allow(unused)] pub struct User { @@ -41,6 +42,16 @@ impl User { } } +impl From for schema::User { + fn from(val: User) -> Self { + schema::User { + email: val.email, + is_admin: val.is_admin, + auth_token: val.auth_token, + } + } +} + impl DbConn { pub async fn create_user( &self,