mirror of https://github.com/coder/coder.git
parent
caf9c41a9e
commit
b7eeb436ad
|
@ -1382,6 +1382,7 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
|
|||
ID: arg.ID,
|
||||
LifetimeSeconds: arg.LifetimeSeconds,
|
||||
HashedSecret: arg.HashedSecret,
|
||||
IPAddress: arg.IPAddress,
|
||||
UserID: arg.UserID,
|
||||
ExpiresAt: arg.ExpiresAt,
|
||||
CreatedAt: arg.CreatedAt,
|
||||
|
@ -1802,6 +1803,7 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
|
|||
}
|
||||
apiKey.LastUsed = arg.LastUsed
|
||||
apiKey.ExpiresAt = arg.ExpiresAt
|
||||
apiKey.IPAddress = arg.IPAddress
|
||||
apiKey.OAuthAccessToken = arg.OAuthAccessToken
|
||||
apiKey.OAuthRefreshToken = arg.OAuthRefreshToken
|
||||
apiKey.OAuthExpiry = arg.OAuthExpiry
|
||||
|
|
|
@ -99,7 +99,8 @@ CREATE TABLE api_keys (
|
|||
oauth_refresh_token text DEFAULT ''::text NOT NULL,
|
||||
oauth_id_token text DEFAULT ''::text NOT NULL,
|
||||
oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
|
||||
lifetime_seconds bigint DEFAULT 86400 NOT NULL
|
||||
lifetime_seconds bigint DEFAULT 86400 NOT NULL,
|
||||
ip_address inet DEFAULT '0.0.0.0'::inet NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE audit_logs (
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE ONLY api_keys
|
||||
DROP COLUMN IF EXISTS ip_address;
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE ONLY api_keys
|
||||
ADD COLUMN IF NOT EXISTS ip_address inet NOT NULL DEFAULT '0.0.0.0';
|
|
@ -311,19 +311,20 @@ func (e *WorkspaceTransition) Scan(src interface{}) error {
|
|||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
|
||||
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
|
||||
OAuthIDToken string `db:"oauth_id_token" json:"oauth_id_token"`
|
||||
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
ID string `db:"id" json:"id"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
|
||||
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
|
||||
OAuthIDToken string `db:"oauth_id_token" json:"oauth_id_token"`
|
||||
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
|
|
|
@ -30,7 +30,7 @@ func (q *sqlQuerier) DeleteAPIKeyByID(ctx context.Context, id string) error {
|
|||
|
||||
const getAPIKeyByID = `-- name: GetAPIKeyByID :one
|
||||
SELECT
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds
|
||||
id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds, ip_address
|
||||
FROM
|
||||
api_keys
|
||||
WHERE
|
||||
|
@ -56,12 +56,13 @@ func (q *sqlQuerier) GetAPIKeyByID(ctx context.Context, id string) (APIKey, erro
|
|||
&i.OAuthIDToken,
|
||||
&i.OAuthExpiry,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAPIKeysLastUsedAfter = `-- name: GetAPIKeysLastUsedAfter :many
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds FROM api_keys WHERE last_used > $1
|
||||
SELECT id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds, ip_address FROM api_keys WHERE last_used > $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error) {
|
||||
|
@ -87,6 +88,7 @@ func (q *sqlQuerier) GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.
|
|||
&i.OAuthIDToken,
|
||||
&i.OAuthExpiry,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -107,6 +109,7 @@ INSERT INTO
|
|||
id,
|
||||
lifetime_seconds,
|
||||
hashed_secret,
|
||||
ip_address,
|
||||
user_id,
|
||||
last_used,
|
||||
expires_at,
|
||||
|
@ -125,23 +128,24 @@ VALUES
|
|||
WHEN 0 THEN 86400
|
||||
ELSE $2::bigint
|
||||
END
|
||||
, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds
|
||||
, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, hashed_secret, user_id, last_used, expires_at, created_at, updated_at, login_type, oauth_access_token, oauth_refresh_token, oauth_id_token, oauth_expiry, lifetime_seconds, ip_address
|
||||
`
|
||||
|
||||
type InsertAPIKeyParams struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
|
||||
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
|
||||
OAuthIDToken string `db:"oauth_id_token" json:"oauth_id_token"`
|
||||
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
|
||||
ID string `db:"id" json:"id"`
|
||||
LifetimeSeconds int64 `db:"lifetime_seconds" json:"lifetime_seconds"`
|
||||
HashedSecret []byte `db:"hashed_secret" json:"hashed_secret"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
LoginType LoginType `db:"login_type" json:"login_type"`
|
||||
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
|
||||
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
|
||||
OAuthIDToken string `db:"oauth_id_token" json:"oauth_id_token"`
|
||||
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (APIKey, error) {
|
||||
|
@ -149,6 +153,7 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
|
|||
arg.ID,
|
||||
arg.LifetimeSeconds,
|
||||
arg.HashedSecret,
|
||||
arg.IPAddress,
|
||||
arg.UserID,
|
||||
arg.LastUsed,
|
||||
arg.ExpiresAt,
|
||||
|
@ -175,6 +180,7 @@ func (q *sqlQuerier) InsertAPIKey(ctx context.Context, arg InsertAPIKeyParams) (
|
|||
&i.OAuthIDToken,
|
||||
&i.OAuthExpiry,
|
||||
&i.LifetimeSeconds,
|
||||
&i.IPAddress,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
@ -185,20 +191,22 @@ UPDATE
|
|||
SET
|
||||
last_used = $2,
|
||||
expires_at = $3,
|
||||
oauth_access_token = $4,
|
||||
oauth_refresh_token = $5,
|
||||
oauth_expiry = $6
|
||||
ip_address = $4,
|
||||
oauth_access_token = $5,
|
||||
oauth_refresh_token = $6,
|
||||
oauth_expiry = $7
|
||||
WHERE
|
||||
id = $1
|
||||
`
|
||||
|
||||
type UpdateAPIKeyByIDParams struct {
|
||||
ID string `db:"id" json:"id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
|
||||
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
|
||||
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
|
||||
ID string `db:"id" json:"id"`
|
||||
LastUsed time.Time `db:"last_used" json:"last_used"`
|
||||
ExpiresAt time.Time `db:"expires_at" json:"expires_at"`
|
||||
IPAddress pqtype.Inet `db:"ip_address" json:"ip_address"`
|
||||
OAuthAccessToken string `db:"oauth_access_token" json:"oauth_access_token"`
|
||||
OAuthRefreshToken string `db:"oauth_refresh_token" json:"oauth_refresh_token"`
|
||||
OAuthExpiry time.Time `db:"oauth_expiry" json:"oauth_expiry"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error {
|
||||
|
@ -206,6 +214,7 @@ func (q *sqlQuerier) UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDP
|
|||
arg.ID,
|
||||
arg.LastUsed,
|
||||
arg.ExpiresAt,
|
||||
arg.IPAddress,
|
||||
arg.OAuthAccessToken,
|
||||
arg.OAuthRefreshToken,
|
||||
arg.OAuthExpiry,
|
||||
|
|
|
@ -17,6 +17,7 @@ INSERT INTO
|
|||
id,
|
||||
lifetime_seconds,
|
||||
hashed_secret,
|
||||
ip_address,
|
||||
user_id,
|
||||
last_used,
|
||||
expires_at,
|
||||
|
@ -35,7 +36,7 @@ VALUES
|
|||
WHEN 0 THEN 86400
|
||||
ELSE @lifetime_seconds::bigint
|
||||
END
|
||||
, @hashed_secret, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @oauth_access_token, @oauth_refresh_token, @oauth_id_token, @oauth_expiry) RETURNING *;
|
||||
, @hashed_secret, @ip_address, @user_id, @last_used, @expires_at, @created_at, @updated_at, @login_type, @oauth_access_token, @oauth_refresh_token, @oauth_id_token, @oauth_expiry) RETURNING *;
|
||||
|
||||
-- name: UpdateAPIKeyByID :exec
|
||||
UPDATE
|
||||
|
@ -43,9 +44,10 @@ UPDATE
|
|||
SET
|
||||
last_used = $2,
|
||||
expires_at = $3,
|
||||
oauth_access_token = $4,
|
||||
oauth_refresh_token = $5,
|
||||
oauth_expiry = $6
|
||||
ip_address = $4,
|
||||
oauth_access_token = $5,
|
||||
oauth_refresh_token = $6,
|
||||
oauth_expiry = $7
|
||||
WHERE
|
||||
id = $1;
|
||||
|
||||
|
|
|
@ -29,3 +29,4 @@ rename:
|
|||
userstatus: UserStatus
|
||||
gitsshkey: GitSSHKey
|
||||
rbac_roles: RBACRoles
|
||||
ip_address: IPAddress
|
||||
|
|
|
@ -7,12 +7,15 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/tabbed/pqtype"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
"github.com/coder/coder/coderd/httpapi"
|
||||
)
|
||||
|
@ -164,6 +167,17 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h
|
|||
// Only update LastUsed once an hour to prevent database spam.
|
||||
if now.Sub(key.LastUsed) > time.Hour {
|
||||
key.LastUsed = now
|
||||
remoteIP := net.ParseIP(r.RemoteAddr)
|
||||
if remoteIP == nil {
|
||||
remoteIP = net.IPv4(0, 0, 0, 0)
|
||||
}
|
||||
key.IPAddress = pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
IP: remoteIP,
|
||||
Mask: remoteIP.DefaultMask(),
|
||||
},
|
||||
Valid: true,
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
// Only update the ExpiresAt once an hour to prevent database spam.
|
||||
|
@ -178,6 +192,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs) func(http.Handler) h
|
|||
ID: key.ID,
|
||||
LastUsed: key.LastUsed,
|
||||
ExpiresAt: key.ExpiresAt,
|
||||
IPAddress: key.IPAddress,
|
||||
OAuthAccessToken: key.OAuthAccessToken,
|
||||
OAuthRefreshToken: key.OAuthRefreshToken,
|
||||
OAuthExpiry: key.OAuthExpiry,
|
||||
|
|
|
@ -402,6 +402,41 @@ func TestAPIKey(t *testing.T) {
|
|||
require.Equal(t, token.Expiry, gotAPIKey.ExpiresAt)
|
||||
require.Equal(t, token.AccessToken, gotAPIKey.OAuthAccessToken)
|
||||
})
|
||||
|
||||
t.Run("RemoteIPUpdates", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var (
|
||||
db = databasefake.New()
|
||||
id, secret = randomAPIKeyParts()
|
||||
hashed = sha256.Sum256([]byte(secret))
|
||||
r = httptest.NewRequest("GET", "/", nil)
|
||||
rw = httptest.NewRecorder()
|
||||
user = createUser(r.Context(), t, db)
|
||||
)
|
||||
r.RemoteAddr = "1.1.1.1"
|
||||
r.AddCookie(&http.Cookie{
|
||||
Name: httpmw.SessionTokenKey,
|
||||
Value: fmt.Sprintf("%s-%s", id, secret),
|
||||
})
|
||||
|
||||
sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: id,
|
||||
HashedSecret: hashed[:],
|
||||
LastUsed: database.Now().AddDate(0, 0, -1),
|
||||
ExpiresAt: database.Now().AddDate(0, 0, 1),
|
||||
UserID: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
httpmw.ExtractAPIKey(db, nil)(successHandler).ServeHTTP(rw, r)
|
||||
res := rw.Result()
|
||||
defer res.Body.Close()
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
|
||||
gotAPIKey, err := db.GetAPIKeyByID(r.Context(), id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotEqual(t, sentAPIKey.IPAddress, gotAPIKey.IPAddress)
|
||||
})
|
||||
}
|
||||
|
||||
func createUser(ctx context.Context, t *testing.T, db database.Store) database.User {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
|
@ -428,13 +429,17 @@ func (r *remoteReporter) createSnapshot() (*Snapshot, error) {
|
|||
|
||||
// ConvertAPIKey anonymizes an API key.
|
||||
func ConvertAPIKey(apiKey database.APIKey) APIKey {
|
||||
return APIKey{
|
||||
a := APIKey{
|
||||
ID: apiKey.ID,
|
||||
UserID: apiKey.UserID,
|
||||
CreatedAt: apiKey.CreatedAt,
|
||||
LastUsed: apiKey.LastUsed,
|
||||
LoginType: apiKey.LoginType,
|
||||
}
|
||||
if apiKey.IPAddress.Valid {
|
||||
a.IPAddress = apiKey.IPAddress.IPNet.IP
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// ConvertWorkspace anonymizes a workspace.
|
||||
|
@ -616,6 +621,7 @@ type APIKey struct {
|
|||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed time.Time `json:"last_used"`
|
||||
LoginType database.LoginType `json:"login_type"`
|
||||
IPAddress net.IP `json:"ip_address"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -13,6 +14,7 @@ import (
|
|||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/tabbed/pqtype"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/database"
|
||||
|
@ -798,10 +800,21 @@ func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
|
|||
}
|
||||
}
|
||||
|
||||
_, err = api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ip := net.ParseIP(r.RemoteAddr)
|
||||
if ip == nil {
|
||||
ip = net.IPv4(0, 0, 0, 0)
|
||||
}
|
||||
key, err := api.Database.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{
|
||||
ID: keyID,
|
||||
UserID: params.UserID,
|
||||
LifetimeSeconds: params.LifetimeSeconds,
|
||||
IPAddress: pqtype.Inet{
|
||||
IPNet: net.IPNet{
|
||||
IP: ip,
|
||||
Mask: ip.DefaultMask(),
|
||||
},
|
||||
Valid: true,
|
||||
},
|
||||
// Make sure in UTC time for common time zone
|
||||
ExpiresAt: params.ExpiresAt.UTC(),
|
||||
CreatedAt: database.Now(),
|
||||
|
@ -821,6 +834,10 @@ func (api *API) createAPIKey(rw http.ResponseWriter, r *http.Request, params dat
|
|||
return "", false
|
||||
}
|
||||
|
||||
api.Telemetry.Report(&telemetry.Snapshot{
|
||||
APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(key)},
|
||||
})
|
||||
|
||||
// This format is consumed by the APIKey middleware.
|
||||
sessionToken := fmt.Sprintf("%s-%s", keyID, keySecret)
|
||||
http.SetCookie(rw, &http.Cookie{
|
||||
|
|
|
@ -175,8 +175,9 @@ func TestPostLogin(t *testing.T) {
|
|||
require.NoError(t, err, "fetch api key")
|
||||
|
||||
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
|
||||
ID: apiKey.ID,
|
||||
LastUsed: apiKey.LastUsed,
|
||||
ID: apiKey.ID,
|
||||
LastUsed: apiKey.LastUsed,
|
||||
IPAddress: apiKey.IPAddress,
|
||||
// This should cause a refresh
|
||||
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
|
||||
OAuthAccessToken: apiKey.OAuthAccessToken,
|
||||
|
@ -207,8 +208,9 @@ func TestPostLogin(t *testing.T) {
|
|||
require.NoError(t, err, "fetch login key")
|
||||
|
||||
err = api.Database.UpdateAPIKeyByID(ctx, database.UpdateAPIKeyByIDParams{
|
||||
ID: apiKey.ID,
|
||||
LastUsed: apiKey.LastUsed,
|
||||
ID: apiKey.ID,
|
||||
LastUsed: apiKey.LastUsed,
|
||||
IPAddress: apiKey.IPAddress,
|
||||
// This should cause a refresh
|
||||
ExpiresAt: apiKey.ExpiresAt.Add(time.Hour * -2),
|
||||
OAuthAccessToken: apiKey.OAuthAccessToken,
|
||||
|
|
Loading…
Reference in New Issue