Add last used at to personal api key
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<76057cc6f78c32b15c6e580c7a0c30da>>
|
* @generated SignedSource<<ee849e769490a70fd3d28e8f809245c2>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -124,6 +124,13 @@ return {
|
|||||||
"name": "expiresAt",
|
"name": "expiresAt",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "lastUsedAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"alias": null,
|
"alias": null,
|
||||||
"args": null,
|
"args": null,
|
||||||
@@ -187,12 +194,12 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "cf6f6f621bdcb1bd24a2dd94f856729f",
|
"cacheID": "4db28039c0fc6e587959d14faf5f3045",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "APIKeysPageQuery",
|
"name": "APIKeysPageQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query APIKeysPageQuery {\n viewer {\n ...PersonalAPIKeyListFragment\n id\n }\n}\n\nfragment PersonalAPIKeyListFragment on Identity {\n id\n personalAPIKeys(first: 1000) {\n edges {\n node {\n id\n ...PersonalAPIKeyRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment PersonalAPIKeyRowFragment on PersonalAPIKey {\n id\n name\n createdAt\n expiresAt\n}\n"
|
"text": "query APIKeysPageQuery {\n viewer {\n ...PersonalAPIKeyListFragment\n id\n }\n}\n\nfragment PersonalAPIKeyListFragment on Identity {\n id\n personalAPIKeys(first: 1000) {\n edges {\n node {\n id\n ...PersonalAPIKeyRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment PersonalAPIKeyRowFragment on PersonalAPIKey {\n id\n name\n createdAt\n expiresAt\n lastUsedAt\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<d4d4692688621656de57fda5884703dd>>
|
* @generated SignedSource<<5899ebfb2c67ad34f132f53990b54136>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -14,6 +14,7 @@ export type PersonalAPIKeyRowFragment$data = {
|
|||||||
readonly createdAt: string;
|
readonly createdAt: string;
|
||||||
readonly expiresAt: string;
|
readonly expiresAt: string;
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
|
readonly lastUsedAt: string | null | undefined;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly token?: string | null | undefined;
|
readonly token?: string | null | undefined;
|
||||||
readonly " $fragmentType": "PersonalAPIKeyRowFragment";
|
readonly " $fragmentType": "PersonalAPIKeyRowFragment";
|
||||||
@@ -77,6 +78,13 @@ const node: ReaderFragment = {
|
|||||||
"name": "expiresAt",
|
"name": "expiresAt",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "lastUsedAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"condition": "includeToken",
|
"condition": "includeToken",
|
||||||
"kind": "Condition",
|
"kind": "Condition",
|
||||||
@@ -96,6 +104,6 @@ const node: ReaderFragment = {
|
|||||||
"abstractKey": null
|
"abstractKey": null
|
||||||
};
|
};
|
||||||
|
|
||||||
(node as any).hash = "d17db443fa203ee5f9c7d0f4576295f0";
|
(node as any).hash = "919daef49b4889be6801516d93f803cf";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* @generated SignedSource<<2d413d4e4cf2528a45ff243c0fa733bb>>
|
* @generated SignedSource<<3f5cf991e78b2be0165979f65ecf845d>>
|
||||||
* @lightSyntaxTransform
|
* @lightSyntaxTransform
|
||||||
* @nogrep
|
* @nogrep
|
||||||
*/
|
*/
|
||||||
@@ -133,6 +133,13 @@ return {
|
|||||||
"name": "expiresAt",
|
"name": "expiresAt",
|
||||||
"storageKey": null
|
"storageKey": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "lastUsedAt",
|
||||||
|
"storageKey": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"condition": "includeToken",
|
"condition": "includeToken",
|
||||||
"kind": "Condition",
|
"kind": "Condition",
|
||||||
@@ -157,16 +164,16 @@ return {
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"cacheID": "b4732244e28d5847baaa5520a95a4cfb",
|
"cacheID": "f9dbf41761006cde825eb68290509c75",
|
||||||
"id": null,
|
"id": null,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"name": "PersonalAPIKeyRowRefetchQuery",
|
"name": "PersonalAPIKeyRowRefetchQuery",
|
||||||
"operationKind": "query",
|
"operationKind": "query",
|
||||||
"text": "query PersonalAPIKeyRowRefetchQuery(\n $includeToken: Boolean = false\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PersonalAPIKeyRowFragment_2T7Twf\n id\n }\n}\n\nfragment PersonalAPIKeyRowFragment_2T7Twf on PersonalAPIKey {\n id\n name\n createdAt\n expiresAt\n token @include(if: $includeToken)\n}\n"
|
"text": "query PersonalAPIKeyRowRefetchQuery(\n $includeToken: Boolean = false\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PersonalAPIKeyRowFragment_2T7Twf\n id\n }\n}\n\nfragment PersonalAPIKeyRowFragment_2T7Twf on PersonalAPIKey {\n id\n name\n createdAt\n expiresAt\n lastUsedAt\n token @include(if: $includeToken)\n}\n"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(node as any).hash = "d17db443fa203ee5f9c7d0f4576295f0";
|
(node as any).hash = "919daef49b4889be6801516d93f803cf";
|
||||||
|
|
||||||
export default node;
|
export default node;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const personalAPIKeyRowFragment = graphql`
|
|||||||
name
|
name
|
||||||
createdAt
|
createdAt
|
||||||
expiresAt
|
expiresAt
|
||||||
|
lastUsedAt
|
||||||
token @include(if: $includeToken)
|
token @include(if: $includeToken)
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -110,6 +111,11 @@ export function PersonalAPIKeyRow(props: {
|
|||||||
{expired ? __("Expired") : __("Active")}
|
{expired ? __("Expired") : __("Active")}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="text-sm text-txt-secondary">
|
||||||
|
{key.lastUsedAt ? formatDate(key.lastUsedAt) : __("Never")}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<span className="text-sm text-txt-secondary">
|
<span className="text-sm text-txt-secondary">
|
||||||
{formatDate(key.createdAt)}
|
{formatDate(key.createdAt)}
|
||||||
|
|||||||
2
pkg/coredata/migrations/20260105T150000Z.sql
Normal file
2
pkg/coredata/migrations/20260105T150000Z.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE iam_personal_api_keys ADD COLUMN last_used_at TIMESTAMPTZ;
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ type (
|
|||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
ExpiresAt time.Time `db:"expires_at"`
|
ExpiresAt time.Time `db:"expires_at"`
|
||||||
ExpireReason *ExpireReason `db:"expire_reason"`
|
ExpireReason *ExpireReason `db:"expire_reason"`
|
||||||
|
LastUsedAt *time.Time `db:"last_used_at"`
|
||||||
CreatedAt time.Time `db:"created_at"`
|
CreatedAt time.Time `db:"created_at"`
|
||||||
UpdatedAt time.Time `db:"updated_at"`
|
UpdatedAt time.Time `db:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -61,6 +62,7 @@ SELECT
|
|||||||
name,
|
name,
|
||||||
expires_at,
|
expires_at,
|
||||||
expire_reason,
|
expire_reason,
|
||||||
|
last_used_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -117,6 +119,7 @@ SELECT
|
|||||||
name,
|
name,
|
||||||
expires_at,
|
expires_at,
|
||||||
expire_reason,
|
expire_reason,
|
||||||
|
last_used_at,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
FROM
|
FROM
|
||||||
@@ -170,13 +173,14 @@ func (a *PersonalAPIKey) Insert(
|
|||||||
) error {
|
) error {
|
||||||
q := `
|
q := `
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
iam_personal_api_keys (id, identity_id, name, expires_at, expire_reason, created_at, updated_at)
|
iam_personal_api_keys (id, identity_id, name, expires_at, expire_reason, last_used_at, created_at, updated_at)
|
||||||
VALUES (
|
VALUES (
|
||||||
@api_key_id,
|
@api_key_id,
|
||||||
@identity_id,
|
@identity_id,
|
||||||
@name,
|
@name,
|
||||||
@expires_at,
|
@expires_at,
|
||||||
@expire_reason,
|
@expire_reason,
|
||||||
|
@last_used_at,
|
||||||
@created_at,
|
@created_at,
|
||||||
@updated_at
|
@updated_at
|
||||||
)
|
)
|
||||||
@@ -188,6 +192,7 @@ VALUES (
|
|||||||
"name": a.Name,
|
"name": a.Name,
|
||||||
"expires_at": a.ExpiresAt,
|
"expires_at": a.ExpiresAt,
|
||||||
"expire_reason": a.ExpireReason,
|
"expire_reason": a.ExpireReason,
|
||||||
|
"last_used_at": a.LastUsedAt,
|
||||||
"created_at": a.CreatedAt,
|
"created_at": a.CreatedAt,
|
||||||
"updated_at": a.UpdatedAt,
|
"updated_at": a.UpdatedAt,
|
||||||
}
|
}
|
||||||
@@ -211,6 +216,7 @@ SET
|
|||||||
name = @name,
|
name = @name,
|
||||||
expires_at = @expires_at,
|
expires_at = @expires_at,
|
||||||
expire_reason = @expire_reason,
|
expire_reason = @expire_reason,
|
||||||
|
last_used_at = @last_used_at,
|
||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
WHERE
|
WHERE
|
||||||
id = @api_key_id
|
id = @api_key_id
|
||||||
@@ -221,6 +227,7 @@ WHERE
|
|||||||
"name": a.Name,
|
"name": a.Name,
|
||||||
"expires_at": a.ExpiresAt,
|
"expires_at": a.ExpiresAt,
|
||||||
"expire_reason": a.ExpireReason,
|
"expire_reason": a.ExpireReason,
|
||||||
|
"last_used_at": a.LastUsedAt,
|
||||||
"updated_at": a.UpdatedAt,
|
"updated_at": a.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ func (s *APIKeyService) GetAPIKey(ctx context.Context, keyID gid.GID) (*coredata
|
|||||||
return NewPersonalAPIKeyExpiredError(keyID)
|
return NewPersonalAPIKeyExpiredError(keyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apiKey.LastUsedAt = &now
|
||||||
|
apiKey.UpdatedAt = now
|
||||||
|
|
||||||
|
if err := apiKey.Update(ctx, tx); err != nil {
|
||||||
|
return fmt.Errorf("cannot update personal api key last used at: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ type PersonalAPIKey implements Node {
|
|||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
expiresAt: Datetime!
|
expiresAt: Datetime!
|
||||||
|
lastUsedAt: Datetime
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
|
|
||||||
token: String @goField(forceResolver: true)
|
token: String @goField(forceResolver: true)
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ type ComplexityRoot struct {
|
|||||||
CreatedAt func(childComplexity int) int
|
CreatedAt func(childComplexity int) int
|
||||||
ExpiresAt func(childComplexity int) int
|
ExpiresAt func(childComplexity int) int
|
||||||
ID func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
|
LastUsedAt func(childComplexity int) int
|
||||||
Name func(childComplexity int) int
|
Name func(childComplexity int) int
|
||||||
Permission func(childComplexity int, action string) int
|
Permission func(childComplexity int, action string) int
|
||||||
Token func(childComplexity int) int
|
Token func(childComplexity int) int
|
||||||
@@ -1530,6 +1531,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.PersonalAPIKey.ID(childComplexity), true
|
return e.complexity.PersonalAPIKey.ID(childComplexity), true
|
||||||
|
case "PersonalAPIKey.lastUsedAt":
|
||||||
|
if e.complexity.PersonalAPIKey.LastUsedAt == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.PersonalAPIKey.LastUsedAt(childComplexity), true
|
||||||
case "PersonalAPIKey.name":
|
case "PersonalAPIKey.name":
|
||||||
if e.complexity.PersonalAPIKey.Name == nil {
|
if e.complexity.PersonalAPIKey.Name == nil {
|
||||||
break
|
break
|
||||||
@@ -2598,6 +2605,7 @@ type PersonalAPIKey implements Node {
|
|||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
expiresAt: Datetime!
|
expiresAt: Datetime!
|
||||||
|
lastUsedAt: Datetime
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
|
|
||||||
token: String @goField(forceResolver: true)
|
token: String @goField(forceResolver: true)
|
||||||
@@ -9070,6 +9078,35 @@ func (ec *executionContext) fieldContext_PersonalAPIKey_expiresAt(_ context.Cont
|
|||||||
return fc, nil
|
return fc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _PersonalAPIKey_lastUsedAt(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) {
|
||||||
|
return graphql.ResolveField(
|
||||||
|
ctx,
|
||||||
|
ec.OperationContext,
|
||||||
|
field,
|
||||||
|
ec.fieldContext_PersonalAPIKey_lastUsedAt,
|
||||||
|
func(ctx context.Context) (any, error) {
|
||||||
|
return obj.LastUsedAt, nil
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
ec.marshalODatetime2ᚖtimeᚐTime,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) fieldContext_PersonalAPIKey_lastUsedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||||
|
fc = &graphql.FieldContext{
|
||||||
|
Object: "PersonalAPIKey",
|
||||||
|
Field: field,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||||
|
return nil, errors.New("field of type Datetime does not have child fields")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _PersonalAPIKey_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) {
|
func (ec *executionContext) _PersonalAPIKey_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) {
|
||||||
return graphql.ResolveField(
|
return graphql.ResolveField(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -9320,6 +9357,8 @@ func (ec *executionContext) fieldContext_PersonalAPIKeyEdge_node(_ context.Conte
|
|||||||
return ec.fieldContext_PersonalAPIKey_name(ctx, field)
|
return ec.fieldContext_PersonalAPIKey_name(ctx, field)
|
||||||
case "expiresAt":
|
case "expiresAt":
|
||||||
return ec.fieldContext_PersonalAPIKey_expiresAt(ctx, field)
|
return ec.fieldContext_PersonalAPIKey_expiresAt(ctx, field)
|
||||||
|
case "lastUsedAt":
|
||||||
|
return ec.fieldContext_PersonalAPIKey_lastUsedAt(ctx, field)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
return ec.fieldContext_PersonalAPIKey_createdAt(ctx, field)
|
return ec.fieldContext_PersonalAPIKey_createdAt(ctx, field)
|
||||||
case "token":
|
case "token":
|
||||||
@@ -17483,6 +17522,8 @@ func (ec *executionContext) _PersonalAPIKey(ctx context.Context, sel ast.Selecti
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
atomic.AddUint32(&out.Invalids, 1)
|
atomic.AddUint32(&out.Invalids, 1)
|
||||||
}
|
}
|
||||||
|
case "lastUsedAt":
|
||||||
|
out.Values[i] = ec._PersonalAPIKey_lastUsedAt(ctx, field, obj)
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
out.Values[i] = ec._PersonalAPIKey_createdAt(ctx, field, obj)
|
out.Values[i] = ec._PersonalAPIKey_createdAt(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ func NewPersonalAPIKey(personalAPIKey *coredata.PersonalAPIKey) *PersonalAPIKey
|
|||||||
ID: personalAPIKey.ID,
|
ID: personalAPIKey.ID,
|
||||||
Name: personalAPIKey.Name,
|
Name: personalAPIKey.Name,
|
||||||
ExpiresAt: personalAPIKey.ExpiresAt,
|
ExpiresAt: personalAPIKey.ExpiresAt,
|
||||||
|
LastUsedAt: personalAPIKey.LastUsedAt,
|
||||||
CreatedAt: personalAPIKey.CreatedAt,
|
CreatedAt: personalAPIKey.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ type PersonalAPIKey struct {
|
|||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ExpiresAt time.Time `json:"expiresAt"`
|
ExpiresAt time.Time `json:"expiresAt"`
|
||||||
|
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Token *string `json:"token,omitempty"`
|
Token *string `json:"token,omitempty"`
|
||||||
Permission bool `json:"permission"`
|
Permission bool `json:"permission"`
|
||||||
|
|||||||
Reference in New Issue
Block a user