From 77d1492aace9566de0395e6d42b011ff4bca1123 Mon Sep 17 00:00:00 2001 From: Nityananda Gohain Date: Fri, 23 May 2025 13:17:20 +0530 Subject: [PATCH] fix: allow non expireable API key (#8013) * fix: allow non expireable API key * fix: clean up pat to API key middleware * fix: address comments * fix: update response of create api key * fix: gettable struct * fix(api-key): frontend changes for api key refactor --------- Co-authored-by: vikrantgupta25 --- ee/http/middleware/api_key.go | 85 +++++++++++++++++++ ee/http/middleware/pat.go | 84 ------------------ ee/modules/user/impluser/handler.go | 14 ++- ee/query-service/app/server.go | 4 +- frontend/src/api/APIKeys/createAPIKey.ts | 26 ------ frontend/src/api/APIKeys/deleteAPIKey.ts | 24 ------ frontend/src/api/APIKeys/getAPIKey.ts | 24 ------ frontend/src/api/APIKeys/getAllAPIKeys.ts | 6 -- frontend/src/api/APIKeys/updateAPIKey.ts | 26 ------ frontend/src/api/v1/pats/create.ts | 28 ++++++ frontend/src/api/v1/pats/delete.ts | 19 +++++ frontend/src/api/v1/pats/list.ts | 20 +++++ frontend/src/api/v1/pats/update.ts | 24 ++++++ frontend/src/container/APIKeys/APIKeys.tsx | 44 +++++----- .../src/hooks/APIKeys/useGetAllAPIKeys.ts | 15 ++-- frontend/src/types/api/pat/types.ts | 9 +- pkg/types/factor_api_key.go | 33 ++++--- 17 files changed, 250 insertions(+), 235 deletions(-) create mode 100644 ee/http/middleware/api_key.go delete mode 100644 ee/http/middleware/pat.go delete mode 100644 frontend/src/api/APIKeys/createAPIKey.ts delete mode 100644 frontend/src/api/APIKeys/deleteAPIKey.ts delete mode 100644 frontend/src/api/APIKeys/getAPIKey.ts delete mode 100644 frontend/src/api/APIKeys/getAllAPIKeys.ts delete mode 100644 frontend/src/api/APIKeys/updateAPIKey.ts create mode 100644 frontend/src/api/v1/pats/create.ts create mode 100644 frontend/src/api/v1/pats/delete.ts create mode 100644 frontend/src/api/v1/pats/list.ts create mode 100644 frontend/src/api/v1/pats/update.ts diff --git a/ee/http/middleware/api_key.go b/ee/http/middleware/api_key.go new file mode 100644 index 0000000000..96e35619a0 --- /dev/null +++ b/ee/http/middleware/api_key.go @@ -0,0 +1,85 @@ +package middleware + +import ( + "net/http" + "time" + + "github.com/SigNoz/signoz/pkg/sqlstore" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "go.uber.org/zap" +) + +type APIKey struct { + store sqlstore.SQLStore + uuid *authtypes.UUID + headers []string +} + +func NewAPIKey(store sqlstore.SQLStore, headers []string) *APIKey { + return &APIKey{store: store, uuid: authtypes.NewUUID(), headers: headers} +} + +func (a *APIKey) Wrap(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var values []string + var apiKeyToken string + var apiKey types.StorableAPIKey + + for _, header := range a.headers { + values = append(values, r.Header.Get(header)) + } + + ctx, err := a.uuid.ContextFromRequest(r.Context(), values...) + if err != nil { + next.ServeHTTP(w, r) + return + } + apiKeyToken, ok := authtypes.UUIDFromContext(ctx) + if !ok { + next.ServeHTTP(w, r) + return + } + + err = a.store.BunDB().NewSelect().Model(&apiKey).Where("token = ?", apiKeyToken).Scan(r.Context()) + if err != nil { + next.ServeHTTP(w, r) + return + } + + // allow the APIKey if expires_at is not set + if apiKey.ExpiresAt.Before(time.Now()) && !apiKey.ExpiresAt.Equal(types.NEVER_EXPIRES) { + next.ServeHTTP(w, r) + return + } + + // get user from db + user := types.User{} + err = a.store.BunDB().NewSelect().Model(&user).Where("id = ?", apiKey.UserID).Scan(r.Context()) + if err != nil { + next.ServeHTTP(w, r) + return + } + + jwt := authtypes.Claims{ + UserID: user.ID.String(), + Role: apiKey.Role, + Email: user.Email, + OrgID: user.OrgID, + } + + ctx = authtypes.NewContextWithClaims(ctx, jwt) + + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + + apiKey.LastUsed = time.Now() + _, err = a.store.BunDB().NewUpdate().Model(&apiKey).Column("last_used").Where("token = ?", apiKeyToken).Where("revoked = false").Exec(r.Context()) + if err != nil { + zap.L().Error("Failed to update APIKey last used in db", zap.Error(err)) + } + + }) + +} diff --git a/ee/http/middleware/pat.go b/ee/http/middleware/pat.go deleted file mode 100644 index 59a18a2b79..0000000000 --- a/ee/http/middleware/pat.go +++ /dev/null @@ -1,84 +0,0 @@ -package middleware - -import ( - "net/http" - "time" - - "github.com/SigNoz/signoz/pkg/sqlstore" - "github.com/SigNoz/signoz/pkg/types" - "github.com/SigNoz/signoz/pkg/types/authtypes" - "go.uber.org/zap" -) - -type Pat struct { - store sqlstore.SQLStore - uuid *authtypes.UUID - headers []string -} - -func NewPat(store sqlstore.SQLStore, headers []string) *Pat { - return &Pat{store: store, uuid: authtypes.NewUUID(), headers: headers} -} - -func (p *Pat) Wrap(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var values []string - var patToken string - var pat types.StorableAPIKey - - for _, header := range p.headers { - values = append(values, r.Header.Get(header)) - } - - ctx, err := p.uuid.ContextFromRequest(r.Context(), values...) - if err != nil { - next.ServeHTTP(w, r) - return - } - patToken, ok := authtypes.UUIDFromContext(ctx) - if !ok { - next.ServeHTTP(w, r) - return - } - - err = p.store.BunDB().NewSelect().Model(&pat).Where("token = ?", patToken).Scan(r.Context()) - if err != nil { - next.ServeHTTP(w, r) - return - } - - if pat.ExpiresAt.Before(time.Now()) { - next.ServeHTTP(w, r) - return - } - - // get user from db - user := types.User{} - err = p.store.BunDB().NewSelect().Model(&user).Where("id = ?", pat.UserID).Scan(r.Context()) - if err != nil { - next.ServeHTTP(w, r) - return - } - - jwt := authtypes.Claims{ - UserID: user.ID.String(), - Role: pat.Role, - Email: user.Email, - OrgID: user.OrgID, - } - - ctx = authtypes.NewContextWithClaims(ctx, jwt) - - r = r.WithContext(ctx) - - next.ServeHTTP(w, r) - - pat.LastUsed = time.Now() - _, err = p.store.BunDB().NewUpdate().Model(&pat).Column("last_used").Where("token = ?", patToken).Where("revoked = false").Exec(r.Context()) - if err != nil { - zap.L().Error("Failed to update PAT last used in db, err: %v", zap.Error(err)) - } - - }) - -} diff --git a/ee/modules/user/impluser/handler.go b/ee/modules/user/impluser/handler.go index 79ebe46b4c..5cb110cb20 100644 --- a/ee/modules/user/impluser/handler.go +++ b/ee/modules/user/impluser/handler.go @@ -215,6 +215,12 @@ func (h *Handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { return } + orgID, err := valuer.NewUUID(claims.OrgID) + if err != nil { + render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "orgId is not a valid uuid-v7")) + return + } + userID, err := valuer.NewUUID(claims.UserID) if err != nil { render.Error(w, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "userId is not a valid uuid-v7")) @@ -244,8 +250,14 @@ func (h *Handler) CreateAPIKey(w http.ResponseWriter, r *http.Request) { return } + createdApiKey, err := h.module.GetAPIKey(ctx, orgID, apiKey.ID) + if err != nil { + render.Error(w, err) + return + } + // just corrected the status code, response is same, - render.Success(w, http.StatusCreated, apiKey) + render.Success(w, http.StatusCreated, createdApiKey) } func (h *Handler) ListAPIKeys(w http.ResponseWriter, r *http.Request) { diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index fb94661b31..3c249cb938 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -258,7 +258,7 @@ func (s *Server) createPrivateServer(apiHandler *api.APIHandler) (*http.Server, r := baseapp.NewRouter() r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap) - r.Use(eemiddleware.NewPat(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap) + r.Use(eemiddleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap) r.Use(middleware.NewTimeout(zap.L(), s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes, s.serverOptions.Config.APIServer.Timeout.Default, @@ -290,7 +290,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h am := middleware.NewAuthZ(s.serverOptions.SigNoz.Instrumentation.Logger()) r.Use(middleware.NewAuth(zap.L(), s.serverOptions.Jwt, []string{"Authorization", "Sec-WebSocket-Protocol"}).Wrap) - r.Use(eemiddleware.NewPat(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap) + r.Use(eemiddleware.NewAPIKey(s.serverOptions.SigNoz.SQLStore, []string{"SIGNOZ-API-KEY"}).Wrap) r.Use(middleware.NewTimeout(zap.L(), s.serverOptions.Config.APIServer.Timeout.ExcludedRoutes, s.serverOptions.Config.APIServer.Timeout.Default, diff --git a/frontend/src/api/APIKeys/createAPIKey.ts b/frontend/src/api/APIKeys/createAPIKey.ts deleted file mode 100644 index 2b219a0166..0000000000 --- a/frontend/src/api/APIKeys/createAPIKey.ts +++ /dev/null @@ -1,26 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { APIKeyProps, CreateAPIKeyProps } from 'types/api/pat/types'; - -const createAPIKey = async ( - props: CreateAPIKeyProps, -): Promise | ErrorResponse> => { - try { - const response = await axios.post('/pats', { - ...props, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default createAPIKey; diff --git a/frontend/src/api/APIKeys/deleteAPIKey.ts b/frontend/src/api/APIKeys/deleteAPIKey.ts deleted file mode 100644 index 03b8d595da..0000000000 --- a/frontend/src/api/APIKeys/deleteAPIKey.ts +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { AllAPIKeyProps } from 'types/api/pat/types'; - -const deleteAPIKey = async ( - id: string, -): Promise | ErrorResponse> => { - try { - const response = await axios.delete(`/pats/${id}`); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default deleteAPIKey; diff --git a/frontend/src/api/APIKeys/getAPIKey.ts b/frontend/src/api/APIKeys/getAPIKey.ts deleted file mode 100644 index c0410d873f..0000000000 --- a/frontend/src/api/APIKeys/getAPIKey.ts +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, Props } from 'types/api/alerts/get'; - -const get = async ( - props: Props, -): Promise | ErrorResponse> => { - try { - const response = await axios.get(`/pats/${props.id}`); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default get; diff --git a/frontend/src/api/APIKeys/getAllAPIKeys.ts b/frontend/src/api/APIKeys/getAllAPIKeys.ts deleted file mode 100644 index 488d9dc5cf..0000000000 --- a/frontend/src/api/APIKeys/getAllAPIKeys.ts +++ /dev/null @@ -1,6 +0,0 @@ -import axios from 'api'; -import { AxiosResponse } from 'axios'; -import { AllAPIKeyProps } from 'types/api/pat/types'; - -export const getAllAPIKeys = (): Promise> => - axios.get(`/pats`); diff --git a/frontend/src/api/APIKeys/updateAPIKey.ts b/frontend/src/api/APIKeys/updateAPIKey.ts deleted file mode 100644 index 38d20227a3..0000000000 --- a/frontend/src/api/APIKeys/updateAPIKey.ts +++ /dev/null @@ -1,26 +0,0 @@ -import axios from 'api'; -import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; -import { AxiosError } from 'axios'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, UpdateAPIKeyProps } from 'types/api/pat/types'; - -const updateAPIKey = async ( - props: UpdateAPIKeyProps, -): Promise | ErrorResponse> => { - try { - const response = await axios.put(`/pats/${props.id}`, { - ...props.data, - }); - - return { - statusCode: 200, - error: null, - message: response.data.status, - payload: response.data.data, - }; - } catch (error) { - return ErrorResponseHandler(error as AxiosError); - } -}; - -export default updateAPIKey; diff --git a/frontend/src/api/v1/pats/create.ts b/frontend/src/api/v1/pats/create.ts new file mode 100644 index 0000000000..c487a91bb3 --- /dev/null +++ b/frontend/src/api/v1/pats/create.ts @@ -0,0 +1,28 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { + APIKeyProps, + CreateAPIKeyProps, + CreatePayloadProps, +} from 'types/api/pat/types'; + +const create = async ( + props: CreateAPIKeyProps, +): Promise> => { + try { + const response = await axios.post('/pats', { + ...props, + }); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default create; diff --git a/frontend/src/api/v1/pats/delete.ts b/frontend/src/api/v1/pats/delete.ts new file mode 100644 index 0000000000..716bdffc19 --- /dev/null +++ b/frontend/src/api/v1/pats/delete.ts @@ -0,0 +1,19 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; + +const deleteAPIKey = async (id: string): Promise> => { + try { + const response = await axios.delete(`/pats/${id}`); + + return { + httpStatusCode: response.status, + data: null, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default deleteAPIKey; diff --git a/frontend/src/api/v1/pats/list.ts b/frontend/src/api/v1/pats/list.ts new file mode 100644 index 0000000000..bc4833af51 --- /dev/null +++ b/frontend/src/api/v1/pats/list.ts @@ -0,0 +1,20 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { AllAPIKeyProps, APIKeyProps } from 'types/api/pat/types'; + +const list = async (): Promise> => { + try { + const response = await axios.get('/pats'); + + return { + httpStatusCode: response.status, + data: response.data.data, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default list; diff --git a/frontend/src/api/v1/pats/update.ts b/frontend/src/api/v1/pats/update.ts new file mode 100644 index 0000000000..b7c1e016c8 --- /dev/null +++ b/frontend/src/api/v1/pats/update.ts @@ -0,0 +1,24 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorV2Resp, SuccessResponseV2 } from 'types/api'; +import { UpdateAPIKeyProps } from 'types/api/pat/types'; + +const updateAPIKey = async ( + props: UpdateAPIKeyProps, +): Promise> => { + try { + const response = await axios.put(`/pats/${props.id}`, { + ...props.data, + }); + + return { + httpStatusCode: response.status, + data: null, + }; + } catch (error) { + ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default updateAPIKey; diff --git a/frontend/src/container/APIKeys/APIKeys.tsx b/frontend/src/container/APIKeys/APIKeys.tsx index 1593908f0e..964e11d8b9 100644 --- a/frontend/src/container/APIKeys/APIKeys.tsx +++ b/frontend/src/container/APIKeys/APIKeys.tsx @@ -20,12 +20,10 @@ import { } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; import { CollapseProps } from 'antd/lib'; -import createAPIKeyApi from 'api/APIKeys/createAPIKey'; -import deleteAPIKeyApi from 'api/APIKeys/deleteAPIKey'; -import updateAPIKeyApi from 'api/APIKeys/updateAPIKey'; -import axios, { AxiosError } from 'axios'; +import createAPIKeyApi from 'api/v1/pats/create'; +import deleteAPIKeyApi from 'api/v1/pats/delete'; +import updateAPIKeyApi from 'api/v1/pats/update'; import cx from 'classnames'; -import { SOMETHING_WENT_WRONG } from 'constants/api'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; import { useGetAllAPIKeys } from 'hooks/APIKeys/useGetAllAPIKeys'; @@ -50,6 +48,7 @@ import { ChangeEvent, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation } from 'react-query'; import { useCopyToClipboard } from 'react-use'; +import APIError from 'types/api/error'; import { APIKeyProps } from 'types/api/pat/types'; import { USER_ROLES } from 'types/roles'; @@ -57,10 +56,11 @@ dayjs.extend(relativeTime); export const showErrorNotification = ( notifications: NotificationInstance, - err: Error, + err: APIError, ): void => { notifications.error({ - message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG, + message: err.getErrorCode(), + description: err.getErrorMessage(), }); }; @@ -177,22 +177,22 @@ function APIKeys(): JSX.Element { } = useGetAllAPIKeys(); useEffect(() => { - setActiveAPIKey(APIKeys?.data.data[0]); + setActiveAPIKey(APIKeys?.data?.[0]); }, [APIKeys]); useEffect(() => { - setDataSource(APIKeys?.data.data || []); - }, [APIKeys?.data.data]); + setDataSource(APIKeys?.data || []); + }, [APIKeys?.data]); useEffect(() => { if (isError) { - showErrorNotification(notifications, error as AxiosError); + showErrorNotification(notifications, error as APIError); } }, [error, isError, notifications]); const handleSearch = (e: ChangeEvent): void => { setSearchValue(e.target.value); - const filteredData = APIKeys?.data?.data?.filter( + const filteredData = APIKeys?.data?.filter( (key: APIKeyProps) => key && key.name && @@ -210,12 +210,12 @@ function APIKeys(): JSX.Element { { onSuccess: (data) => { setShowNewAPIKeyDetails(true); - setActiveAPIKey(data.payload); + setActiveAPIKey(data.data); refetchAPIKeys(); }, onError: (error) => { - showErrorNotification(notifications, error as AxiosError); + showErrorNotification(notifications, error as APIError); }, }, ); @@ -228,7 +228,7 @@ function APIKeys(): JSX.Element { setIsEditModalOpen(false); }, onError: (error) => { - showErrorNotification(notifications, error as AxiosError); + showErrorNotification(notifications, error as APIError); }, }, ); @@ -241,7 +241,7 @@ function APIKeys(): JSX.Element { setIsDeleteModalOpen(false); }, onError: (error) => { - showErrorNotification(notifications, error as AxiosError); + showErrorNotification(notifications, error as APIError); }, }, ); @@ -445,10 +445,12 @@ function APIKeys(): JSX.Element { Creator - {APIKey?.createdByUser?.name?.substring(0, 1)} + {APIKey?.createdByUser?.displayName?.substring(0, 1)} - {APIKey.createdByUser?.name} + + {APIKey.createdByUser?.displayName} +
{APIKey.createdByUser?.email}
@@ -829,10 +831,12 @@ function APIKeys(): JSX.Element { - {activeAPIKey?.createdByUser?.name?.substring(0, 1)} + {activeAPIKey?.createdByUser?.displayName?.substring(0, 1)} - {activeAPIKey?.createdByUser?.name} + + {activeAPIKey?.createdByUser?.displayName} +
{activeAPIKey?.createdByUser?.email}
diff --git a/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts b/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts index c4a0d290a2..802db391dd 100644 --- a/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts +++ b/frontend/src/hooks/APIKeys/useGetAllAPIKeys.ts @@ -1,13 +1,14 @@ -import { getAllAPIKeys } from 'api/APIKeys/getAllAPIKeys'; -import { AxiosError, AxiosResponse } from 'axios'; +import list from 'api/v1/pats/list'; import { useQuery, UseQueryResult } from 'react-query'; -import { AllAPIKeyProps } from 'types/api/pat/types'; +import { SuccessResponseV2 } from 'types/api'; +import APIError from 'types/api/error'; +import { APIKeyProps } from 'types/api/pat/types'; export const useGetAllAPIKeys = (): UseQueryResult< - AxiosResponse, - AxiosError + SuccessResponseV2, + APIError > => - useQuery, AxiosError>({ + useQuery, APIError>({ queryKey: ['APIKeys'], - queryFn: () => getAllAPIKeys(), + queryFn: () => list(), }); diff --git a/frontend/src/types/api/pat/types.ts b/frontend/src/types/api/pat/types.ts index 8226576c69..768ea44b43 100644 --- a/frontend/src/types/api/pat/types.ts +++ b/frontend/src/types/api/pat/types.ts @@ -2,9 +2,7 @@ export interface User { createdAt?: number; email?: string; id: string; - name?: string; - notFound?: boolean; - profilePictureURL?: string; + displayName?: string; } export interface APIKeyProps { @@ -20,6 +18,11 @@ export interface APIKeyProps { lastUsed?: number; } +export interface CreatePayloadProps { + data: APIKeyProps; + status: string; +} + export interface CreateAPIKeyProps { name: string; expiresInDays: number; diff --git a/pkg/types/factor_api_key.go b/pkg/types/factor_api_key.go index 01eb6165fc..e1eb05db69 100644 --- a/pkg/types/factor_api_key.go +++ b/pkg/types/factor_api_key.go @@ -10,6 +10,8 @@ import ( "github.com/uptrace/bun" ) +var NEVER_EXPIRES = time.Unix(0, 0) + type PostableAPIKey struct { Name string `json:"name"` Role Role `json:"role"` @@ -20,15 +22,15 @@ type GettableAPIKey struct { Identifiable TimeAuditable UserAuditable - Token string `json:"token"` - Role Role `json:"role"` - Name string `json:"name"` - ExpiresAt int64 `json:"expiresAt"` - LastUsed int64 `json:"lastUsed"` - Revoked bool `json:"revoked"` - UserID string `json:"userId"` - CreatedBy *User `json:"createdBy"` - UpdatedBy *User `json:"updatedBy"` + Token string `json:"token"` + Role Role `json:"role"` + Name string `json:"name"` + ExpiresAt int64 `json:"expiresAt"` + LastUsed int64 `json:"lastUsed"` + Revoked bool `json:"revoked"` + UserID string `json:"userId"` + CreatedByUser *User `json:"createdByUser"` + UpdatedByUser *User `json:"updatedByUser"` } type OrgUserAPIKey struct { @@ -65,7 +67,9 @@ type StorableAPIKey struct { func NewStorableAPIKey(name string, userID valuer.UUID, role Role, expiresAt int64) (*StorableAPIKey, error) { // validate - if expiresAt <= 0 { + + // we allow the APIKey if expiresAt is not set, which means it never expires + if expiresAt < 0 { return nil, errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "expiresAt must be greater than 0") } @@ -82,6 +86,11 @@ func NewStorableAPIKey(name string, userID valuer.UUID, role Role, expiresAt int // expiresAt = now.Unix() + (expiresAt * 24 * 60 * 60) expiresAtTime := now.AddDate(0, 0, int(expiresAt)) + // if the expiresAt is 0, it means the APIKey never expires + if expiresAt == 0 { + expiresAtTime = NEVER_EXPIRES + } + // Generate a 32-byte random token. token := make([]byte, 32) _, err := rand.Read(token) @@ -129,7 +138,7 @@ func NewGettableAPIKeyFromStorableAPIKey(storableAPIKey *StorableAPIKeyUser) *Ge LastUsed: lastUsed, Revoked: storableAPIKey.Revoked, UserID: storableAPIKey.UserID.String(), - CreatedBy: storableAPIKey.CreatedByUser, - UpdatedBy: storableAPIKey.UpdatedByUser, + CreatedByUser: storableAPIKey.CreatedByUser, + UpdatedByUser: storableAPIKey.UpdatedByUser, } }