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
r0.7
Meng Zhang 2023-12-09 18:12:54 +08:00 committed by GitHub
parent b26a7f7cf3
commit 8dc09ea4e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 104 additions and 686 deletions

View File

@ -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<any, any>> =
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never

View File

@ -1,511 +0,0 @@
/* eslint-disable */
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
export type Maybe<T> = T | null
export type InputMaybe<T> = Maybe<T>
export type Exact<T extends { [key: string]: unknown }> = {
[K in keyof T]: T[K]
}
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & {
[SubKey in K]?: Maybe<T[SubKey]>
}
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & {
[SubKey in K]: Maybe<T[SubKey]>
}
export type MakeEmpty<
T extends { [key: string]: unknown },
K extends keyof T
> = { [_ in K]?: never }
export type Incremental<T> =
| 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<Scalars['String']['input']>
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<Invitation>
isAdminInitialized: Scalars['Boolean']['output']
me: UserInfo
registrationToken: Scalars['String']['output']
workers: Array<Worker>
}
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<Scalars['String']['output']>
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<Scalars['String']['input']>
}>
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<string>
}>
}
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<TokenAuthMutation, TokenAuthMutationVariables>
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<RegisterMutation, RegisterMutationVariables>
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<GetWorkersQuery, GetWorkersQueryVariables>
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
>

View File

@ -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<JwtPayload & { is_admin: boolean }>(
authState.data.accessToken
)
return {
data: {
email: user.email,
isAdmin: user.is_admin,
email: sub!,
isAdmin: is_admin,
accessToken: authState.data.accessToken
},
status: authState.status

View File

@ -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 {

View File

@ -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<String> {
pub fn generate_jwt(claims: JWTPayload) -> jwt::errors::Result<String> {
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<Claims> {
pub fn validate_jwt(token: &str) -> jwt::errors::Result<JWTPayload> {
let validation = jwt::Validation::default();
let data = jwt::decode::<Claims>(token, &JWT_DECODING_KEY, &validation)?;
let data = jwt::decode::<JWTPayload>(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<RefreshTokenResponse, RefreshTokenError>;
async fn verify_access_token(&self, access_token: &str) -> Result<VerifyTokenResponse>;
async fn is_admin_initialized(&self) -> Result<bool>;
async fn get_user_by_email(&self, email: &str) -> Result<User>;
async fn create_invitation(&self, email: String) -> Result<i32>;
async fn list_invitations(&self) -> Result<Vec<Invitation>>;
@ -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]

View File

@ -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<auth::Claims>,
claims: Option<auth::JWTPayload>,
locator: Arc<dyn ServiceLocator>,
}
@ -73,7 +73,7 @@ pub struct Query;
impl Query {
async fn workers(ctx: &Context) -> Result<Vec<Worker>> {
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<String> {
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<Vec<Invitation>> {
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,12 +110,21 @@ impl Query {
))
}
async fn me(ctx: &Context) -> Result<UserInfo> {
async fn me(ctx: &Context) -> Result<User> {
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"))
}
}
}
#[derive(Debug, GraphQLObject)]
pub struct User {
pub email: String,
pub is_admin: bool,
pub auth_token: String,
}
#[derive(Default)]
@ -125,7 +134,7 @@ pub struct Mutation;
impl Mutation {
async fn reset_registration_token(ctx: &Context) -> Result<String> {
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<i32> {
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<i32> {
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?);
}
}

View File

@ -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<User> {
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<i32> {
self.create_invitation(email).await
}

View File

@ -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<User> 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,