Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
7be11952
Commit
7be11952
authored
Feb 22, 2026
by
yangjianbo
Browse files
feat(api-key): 增加 API Key 上次使用时间并补齐测试
parent
1fae8d08
Changes
29
Hide whitespace changes
Inline
Side-by-side
backend/ent/apikey.go
View file @
7be11952
...
...
@@ -36,6 +36,8 @@ type APIKey struct {
GroupID
*
int64
`json:"group_id,omitempty"`
// Status holds the value of the "status" field.
Status
string
`json:"status,omitempty"`
// Last usage time of this API key
LastUsedAt
*
time
.
Time
`json:"last_used_at,omitempty"`
// Allowed IPs/CIDRs, e.g. ["192.168.1.100", "10.0.0.0/8"]
IPWhitelist
[]
string
`json:"ip_whitelist,omitempty"`
// Blocked IPs/CIDRs
...
...
@@ -109,7 +111,7 @@ func (*APIKey) scanValues(columns []string) ([]any, error) {
values
[
i
]
=
new
(
sql
.
NullInt64
)
case
apikey
.
FieldKey
,
apikey
.
FieldName
,
apikey
.
FieldStatus
:
values
[
i
]
=
new
(
sql
.
NullString
)
case
apikey
.
FieldCreatedAt
,
apikey
.
FieldUpdatedAt
,
apikey
.
FieldDeletedAt
,
apikey
.
FieldExpiresAt
:
case
apikey
.
FieldCreatedAt
,
apikey
.
FieldUpdatedAt
,
apikey
.
FieldDeletedAt
,
apikey
.
FieldLastUsedAt
,
apikey
.
FieldExpiresAt
:
values
[
i
]
=
new
(
sql
.
NullTime
)
default
:
values
[
i
]
=
new
(
sql
.
UnknownType
)
...
...
@@ -182,6 +184,13 @@ func (_m *APIKey) assignValues(columns []string, values []any) error {
}
else
if
value
.
Valid
{
_m
.
Status
=
value
.
String
}
case
apikey
.
FieldLastUsedAt
:
if
value
,
ok
:=
values
[
i
]
.
(
*
sql
.
NullTime
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field last_used_at"
,
values
[
i
])
}
else
if
value
.
Valid
{
_m
.
LastUsedAt
=
new
(
time
.
Time
)
*
_m
.
LastUsedAt
=
value
.
Time
}
case
apikey
.
FieldIPWhitelist
:
if
value
,
ok
:=
values
[
i
]
.
(
*
[]
byte
);
!
ok
{
return
fmt
.
Errorf
(
"unexpected type %T for field ip_whitelist"
,
values
[
i
])
...
...
@@ -296,6 +305,11 @@ func (_m *APIKey) String() string {
builder
.
WriteString
(
"status="
)
builder
.
WriteString
(
_m
.
Status
)
builder
.
WriteString
(
", "
)
if
v
:=
_m
.
LastUsedAt
;
v
!=
nil
{
builder
.
WriteString
(
"last_used_at="
)
builder
.
WriteString
(
v
.
Format
(
time
.
ANSIC
))
}
builder
.
WriteString
(
", "
)
builder
.
WriteString
(
"ip_whitelist="
)
builder
.
WriteString
(
fmt
.
Sprintf
(
"%v"
,
_m
.
IPWhitelist
))
builder
.
WriteString
(
", "
)
...
...
backend/ent/apikey/apikey.go
View file @
7be11952
...
...
@@ -31,6 +31,8 @@ const (
FieldGroupID
=
"group_id"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus
=
"status"
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
FieldLastUsedAt
=
"last_used_at"
// FieldIPWhitelist holds the string denoting the ip_whitelist field in the database.
FieldIPWhitelist
=
"ip_whitelist"
// FieldIPBlacklist holds the string denoting the ip_blacklist field in the database.
...
...
@@ -83,6 +85,7 @@ var Columns = []string{
FieldName
,
FieldGroupID
,
FieldStatus
,
FieldLastUsedAt
,
FieldIPWhitelist
,
FieldIPBlacklist
,
FieldQuota
,
...
...
@@ -176,6 +179,11 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return
sql
.
OrderByField
(
FieldStatus
,
opts
...
)
.
ToFunc
()
}
// ByLastUsedAt orders the results by the last_used_at field.
func
ByLastUsedAt
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldLastUsedAt
,
opts
...
)
.
ToFunc
()
}
// ByQuota orders the results by the quota field.
func
ByQuota
(
opts
...
sql
.
OrderTermOption
)
OrderOption
{
return
sql
.
OrderByField
(
FieldQuota
,
opts
...
)
.
ToFunc
()
...
...
backend/ent/apikey/where.go
View file @
7be11952
...
...
@@ -95,6 +95,11 @@ func Status(v string) predicate.APIKey {
return
predicate
.
APIKey
(
sql
.
FieldEQ
(
FieldStatus
,
v
))
}
// LastUsedAt applies equality check predicate on the "last_used_at" field. It's identical to LastUsedAtEQ.
func
LastUsedAt
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldEQ
(
FieldLastUsedAt
,
v
))
}
// Quota applies equality check predicate on the "quota" field. It's identical to QuotaEQ.
func
Quota
(
v
float64
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldEQ
(
FieldQuota
,
v
))
...
...
@@ -485,6 +490,56 @@ func StatusContainsFold(v string) predicate.APIKey {
return
predicate
.
APIKey
(
sql
.
FieldContainsFold
(
FieldStatus
,
v
))
}
// LastUsedAtEQ applies the EQ predicate on the "last_used_at" field.
func
LastUsedAtEQ
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldEQ
(
FieldLastUsedAt
,
v
))
}
// LastUsedAtNEQ applies the NEQ predicate on the "last_used_at" field.
func
LastUsedAtNEQ
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldNEQ
(
FieldLastUsedAt
,
v
))
}
// LastUsedAtIn applies the In predicate on the "last_used_at" field.
func
LastUsedAtIn
(
vs
...
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldIn
(
FieldLastUsedAt
,
vs
...
))
}
// LastUsedAtNotIn applies the NotIn predicate on the "last_used_at" field.
func
LastUsedAtNotIn
(
vs
...
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldNotIn
(
FieldLastUsedAt
,
vs
...
))
}
// LastUsedAtGT applies the GT predicate on the "last_used_at" field.
func
LastUsedAtGT
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldGT
(
FieldLastUsedAt
,
v
))
}
// LastUsedAtGTE applies the GTE predicate on the "last_used_at" field.
func
LastUsedAtGTE
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldGTE
(
FieldLastUsedAt
,
v
))
}
// LastUsedAtLT applies the LT predicate on the "last_used_at" field.
func
LastUsedAtLT
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldLT
(
FieldLastUsedAt
,
v
))
}
// LastUsedAtLTE applies the LTE predicate on the "last_used_at" field.
func
LastUsedAtLTE
(
v
time
.
Time
)
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldLTE
(
FieldLastUsedAt
,
v
))
}
// LastUsedAtIsNil applies the IsNil predicate on the "last_used_at" field.
func
LastUsedAtIsNil
()
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldIsNull
(
FieldLastUsedAt
))
}
// LastUsedAtNotNil applies the NotNil predicate on the "last_used_at" field.
func
LastUsedAtNotNil
()
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldNotNull
(
FieldLastUsedAt
))
}
// IPWhitelistIsNil applies the IsNil predicate on the "ip_whitelist" field.
func
IPWhitelistIsNil
()
predicate
.
APIKey
{
return
predicate
.
APIKey
(
sql
.
FieldIsNull
(
FieldIPWhitelist
))
...
...
backend/ent/apikey_create.go
View file @
7be11952
...
...
@@ -113,6 +113,20 @@ func (_c *APIKeyCreate) SetNillableStatus(v *string) *APIKeyCreate {
return
_c
}
// SetLastUsedAt sets the "last_used_at" field.
func
(
_c
*
APIKeyCreate
)
SetLastUsedAt
(
v
time
.
Time
)
*
APIKeyCreate
{
_c
.
mutation
.
SetLastUsedAt
(
v
)
return
_c
}
// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil.
func
(
_c
*
APIKeyCreate
)
SetNillableLastUsedAt
(
v
*
time
.
Time
)
*
APIKeyCreate
{
if
v
!=
nil
{
_c
.
SetLastUsedAt
(
*
v
)
}
return
_c
}
// SetIPWhitelist sets the "ip_whitelist" field.
func
(
_c
*
APIKeyCreate
)
SetIPWhitelist
(
v
[]
string
)
*
APIKeyCreate
{
_c
.
mutation
.
SetIPWhitelist
(
v
)
...
...
@@ -353,6 +367,10 @@ func (_c *APIKeyCreate) createSpec() (*APIKey, *sqlgraph.CreateSpec) {
_spec
.
SetField
(
apikey
.
FieldStatus
,
field
.
TypeString
,
value
)
_node
.
Status
=
value
}
if
value
,
ok
:=
_c
.
mutation
.
LastUsedAt
();
ok
{
_spec
.
SetField
(
apikey
.
FieldLastUsedAt
,
field
.
TypeTime
,
value
)
_node
.
LastUsedAt
=
&
value
}
if
value
,
ok
:=
_c
.
mutation
.
IPWhitelist
();
ok
{
_spec
.
SetField
(
apikey
.
FieldIPWhitelist
,
field
.
TypeJSON
,
value
)
_node
.
IPWhitelist
=
value
...
...
@@ -571,6 +589,24 @@ func (u *APIKeyUpsert) UpdateStatus() *APIKeyUpsert {
return
u
}
// SetLastUsedAt sets the "last_used_at" field.
func
(
u
*
APIKeyUpsert
)
SetLastUsedAt
(
v
time
.
Time
)
*
APIKeyUpsert
{
u
.
Set
(
apikey
.
FieldLastUsedAt
,
v
)
return
u
}
// UpdateLastUsedAt sets the "last_used_at" field to the value that was provided on create.
func
(
u
*
APIKeyUpsert
)
UpdateLastUsedAt
()
*
APIKeyUpsert
{
u
.
SetExcluded
(
apikey
.
FieldLastUsedAt
)
return
u
}
// ClearLastUsedAt clears the value of the "last_used_at" field.
func
(
u
*
APIKeyUpsert
)
ClearLastUsedAt
()
*
APIKeyUpsert
{
u
.
SetNull
(
apikey
.
FieldLastUsedAt
)
return
u
}
// SetIPWhitelist sets the "ip_whitelist" field.
func
(
u
*
APIKeyUpsert
)
SetIPWhitelist
(
v
[]
string
)
*
APIKeyUpsert
{
u
.
Set
(
apikey
.
FieldIPWhitelist
,
v
)
...
...
@@ -818,6 +854,27 @@ func (u *APIKeyUpsertOne) UpdateStatus() *APIKeyUpsertOne {
})
}
// SetLastUsedAt sets the "last_used_at" field.
func
(
u
*
APIKeyUpsertOne
)
SetLastUsedAt
(
v
time
.
Time
)
*
APIKeyUpsertOne
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
s
.
SetLastUsedAt
(
v
)
})
}
// UpdateLastUsedAt sets the "last_used_at" field to the value that was provided on create.
func
(
u
*
APIKeyUpsertOne
)
UpdateLastUsedAt
()
*
APIKeyUpsertOne
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
s
.
UpdateLastUsedAt
()
})
}
// ClearLastUsedAt clears the value of the "last_used_at" field.
func
(
u
*
APIKeyUpsertOne
)
ClearLastUsedAt
()
*
APIKeyUpsertOne
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
s
.
ClearLastUsedAt
()
})
}
// SetIPWhitelist sets the "ip_whitelist" field.
func
(
u
*
APIKeyUpsertOne
)
SetIPWhitelist
(
v
[]
string
)
*
APIKeyUpsertOne
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
...
...
@@ -1246,6 +1303,27 @@ func (u *APIKeyUpsertBulk) UpdateStatus() *APIKeyUpsertBulk {
})
}
// SetLastUsedAt sets the "last_used_at" field.
func
(
u
*
APIKeyUpsertBulk
)
SetLastUsedAt
(
v
time
.
Time
)
*
APIKeyUpsertBulk
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
s
.
SetLastUsedAt
(
v
)
})
}
// UpdateLastUsedAt sets the "last_used_at" field to the value that was provided on create.
func
(
u
*
APIKeyUpsertBulk
)
UpdateLastUsedAt
()
*
APIKeyUpsertBulk
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
s
.
UpdateLastUsedAt
()
})
}
// ClearLastUsedAt clears the value of the "last_used_at" field.
func
(
u
*
APIKeyUpsertBulk
)
ClearLastUsedAt
()
*
APIKeyUpsertBulk
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
s
.
ClearLastUsedAt
()
})
}
// SetIPWhitelist sets the "ip_whitelist" field.
func
(
u
*
APIKeyUpsertBulk
)
SetIPWhitelist
(
v
[]
string
)
*
APIKeyUpsertBulk
{
return
u
.
Update
(
func
(
s
*
APIKeyUpsert
)
{
...
...
backend/ent/apikey_update.go
View file @
7be11952
...
...
@@ -134,6 +134,26 @@ func (_u *APIKeyUpdate) SetNillableStatus(v *string) *APIKeyUpdate {
return
_u
}
// SetLastUsedAt sets the "last_used_at" field.
func
(
_u
*
APIKeyUpdate
)
SetLastUsedAt
(
v
time
.
Time
)
*
APIKeyUpdate
{
_u
.
mutation
.
SetLastUsedAt
(
v
)
return
_u
}
// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil.
func
(
_u
*
APIKeyUpdate
)
SetNillableLastUsedAt
(
v
*
time
.
Time
)
*
APIKeyUpdate
{
if
v
!=
nil
{
_u
.
SetLastUsedAt
(
*
v
)
}
return
_u
}
// ClearLastUsedAt clears the value of the "last_used_at" field.
func
(
_u
*
APIKeyUpdate
)
ClearLastUsedAt
()
*
APIKeyUpdate
{
_u
.
mutation
.
ClearLastUsedAt
()
return
_u
}
// SetIPWhitelist sets the "ip_whitelist" field.
func
(
_u
*
APIKeyUpdate
)
SetIPWhitelist
(
v
[]
string
)
*
APIKeyUpdate
{
_u
.
mutation
.
SetIPWhitelist
(
v
)
...
...
@@ -390,6 +410,12 @@ func (_u *APIKeyUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
_spec
.
SetField
(
apikey
.
FieldStatus
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
LastUsedAt
();
ok
{
_spec
.
SetField
(
apikey
.
FieldLastUsedAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
LastUsedAtCleared
()
{
_spec
.
ClearField
(
apikey
.
FieldLastUsedAt
,
field
.
TypeTime
)
}
if
value
,
ok
:=
_u
.
mutation
.
IPWhitelist
();
ok
{
_spec
.
SetField
(
apikey
.
FieldIPWhitelist
,
field
.
TypeJSON
,
value
)
}
...
...
@@ -655,6 +681,26 @@ func (_u *APIKeyUpdateOne) SetNillableStatus(v *string) *APIKeyUpdateOne {
return
_u
}
// SetLastUsedAt sets the "last_used_at" field.
func
(
_u
*
APIKeyUpdateOne
)
SetLastUsedAt
(
v
time
.
Time
)
*
APIKeyUpdateOne
{
_u
.
mutation
.
SetLastUsedAt
(
v
)
return
_u
}
// SetNillableLastUsedAt sets the "last_used_at" field if the given value is not nil.
func
(
_u
*
APIKeyUpdateOne
)
SetNillableLastUsedAt
(
v
*
time
.
Time
)
*
APIKeyUpdateOne
{
if
v
!=
nil
{
_u
.
SetLastUsedAt
(
*
v
)
}
return
_u
}
// ClearLastUsedAt clears the value of the "last_used_at" field.
func
(
_u
*
APIKeyUpdateOne
)
ClearLastUsedAt
()
*
APIKeyUpdateOne
{
_u
.
mutation
.
ClearLastUsedAt
()
return
_u
}
// SetIPWhitelist sets the "ip_whitelist" field.
func
(
_u
*
APIKeyUpdateOne
)
SetIPWhitelist
(
v
[]
string
)
*
APIKeyUpdateOne
{
_u
.
mutation
.
SetIPWhitelist
(
v
)
...
...
@@ -941,6 +987,12 @@ func (_u *APIKeyUpdateOne) sqlSave(ctx context.Context) (_node *APIKey, err erro
if
value
,
ok
:=
_u
.
mutation
.
Status
();
ok
{
_spec
.
SetField
(
apikey
.
FieldStatus
,
field
.
TypeString
,
value
)
}
if
value
,
ok
:=
_u
.
mutation
.
LastUsedAt
();
ok
{
_spec
.
SetField
(
apikey
.
FieldLastUsedAt
,
field
.
TypeTime
,
value
)
}
if
_u
.
mutation
.
LastUsedAtCleared
()
{
_spec
.
ClearField
(
apikey
.
FieldLastUsedAt
,
field
.
TypeTime
)
}
if
value
,
ok
:=
_u
.
mutation
.
IPWhitelist
();
ok
{
_spec
.
SetField
(
apikey
.
FieldIPWhitelist
,
field
.
TypeJSON
,
value
)
}
...
...
backend/ent/migrate/schema.go
View file @
7be11952
...
...
@@ -18,6 +18,7 @@ var (
{
Name
:
"key"
,
Type
:
field
.
TypeString
,
Unique
:
true
,
Size
:
128
},
{
Name
:
"name"
,
Type
:
field
.
TypeString
,
Size
:
100
},
{
Name
:
"status"
,
Type
:
field
.
TypeString
,
Size
:
20
,
Default
:
"active"
},
{
Name
:
"last_used_at"
,
Type
:
field
.
TypeTime
,
Nullable
:
true
},
{
Name
:
"ip_whitelist"
,
Type
:
field
.
TypeJSON
,
Nullable
:
true
},
{
Name
:
"ip_blacklist"
,
Type
:
field
.
TypeJSON
,
Nullable
:
true
},
{
Name
:
"quota"
,
Type
:
field
.
TypeFloat64
,
Default
:
0
,
SchemaType
:
map
[
string
]
string
{
"postgres"
:
"decimal(20,8)"
}},
...
...
@@ -34,13 +35,13 @@ var (
ForeignKeys
:
[]
*
schema
.
ForeignKey
{
{
Symbol
:
"api_keys_groups_api_keys"
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
2
]},
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
3
]},
RefColumns
:
[]
*
schema
.
Column
{
GroupsColumns
[
0
]},
OnDelete
:
schema
.
SetNull
,
},
{
Symbol
:
"api_keys_users_api_keys"
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
3
]},
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
4
]},
RefColumns
:
[]
*
schema
.
Column
{
UsersColumns
[
0
]},
OnDelete
:
schema
.
NoAction
,
},
...
...
@@ -49,12 +50,12 @@ var (
{
Name
:
"apikey_user_id"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
3
]},
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
4
]},
},
{
Name
:
"apikey_group_id"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
2
]},
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
3
]},
},
{
Name
:
"apikey_status"
,
...
...
@@ -66,15 +67,20 @@ var (
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
3
]},
},
{
Name
:
"apikey_last_used_at"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
7
]},
},
{
Name
:
"apikey_quota_quota_used"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
9
],
APIKeysColumns
[
1
0
]},
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
10
],
APIKeysColumns
[
1
1
]},
},
{
Name
:
"apikey_expires_at"
,
Unique
:
false
,
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
1
]},
Columns
:
[]
*
schema
.
Column
{
APIKeysColumns
[
1
2
]},
},
},
}
...
...
backend/ent/mutation.go
View file @
7be11952
...
...
@@ -79,6 +79,7 @@ type APIKeyMutation struct {
key *string
name *string
status *string
last_used_at *time.Time
ip_whitelist *[]string
appendip_whitelist []string
ip_blacklist *[]string
...
...
@@ -513,6 +514,55 @@ func (m *APIKeyMutation) ResetStatus() {
m.status = nil
}
// SetLastUsedAt sets the "last_used_at" field.
func (m *APIKeyMutation) SetLastUsedAt(t time.Time) {
m.last_used_at = &t
}
// LastUsedAt returns the value of the "last_used_at" field in the mutation.
func (m *APIKeyMutation) LastUsedAt() (r time.Time, exists bool) {
v := m.last_used_at
if v == nil {
return
}
return *v, true
}
// OldLastUsedAt returns the old "last_used_at" field's value of the APIKey entity.
// If the APIKey object wasn't provided to the builder, the object is fetched from the database.
// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
func (m *APIKeyMutation) OldLastUsedAt(ctx context.Context) (v *time.Time, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldLastUsedAt is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldLastUsedAt requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldLastUsedAt: %w", err)
}
return oldValue.LastUsedAt, nil
}
// ClearLastUsedAt clears the value of the "last_used_at" field.
func (m *APIKeyMutation) ClearLastUsedAt() {
m.last_used_at = nil
m.clearedFields[apikey.FieldLastUsedAt] = struct{}{}
}
// LastUsedAtCleared returns if the "last_used_at" field was cleared in this mutation.
func (m *APIKeyMutation) LastUsedAtCleared() bool {
_, ok := m.clearedFields[apikey.FieldLastUsedAt]
return ok
}
// ResetLastUsedAt resets all changes to the "last_used_at" field.
func (m *APIKeyMutation) ResetLastUsedAt() {
m.last_used_at = nil
delete(m.clearedFields, apikey.FieldLastUsedAt)
}
// SetIPWhitelist sets the "ip_whitelist" field.
func (m *APIKeyMutation) SetIPWhitelist(s []string) {
m.ip_whitelist = &s
...
...
@@ -946,7 +996,7 @@ func (m *APIKeyMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *APIKeyMutation) Fields() []string {
fields := make([]string, 0, 1
3
)
fields := make([]string, 0, 1
4
)
if m.created_at != nil {
fields = append(fields, apikey.FieldCreatedAt)
}
...
...
@@ -971,6 +1021,9 @@ func (m *APIKeyMutation) Fields() []string {
if m.status != nil {
fields = append(fields, apikey.FieldStatus)
}
if m.last_used_at != nil {
fields = append(fields, apikey.FieldLastUsedAt)
}
if m.ip_whitelist != nil {
fields = append(fields, apikey.FieldIPWhitelist)
}
...
...
@@ -1010,6 +1063,8 @@ func (m *APIKeyMutation) Field(name string) (ent.Value, bool) {
return m.GroupID()
case apikey.FieldStatus:
return m.Status()
case apikey.FieldLastUsedAt:
return m.LastUsedAt()
case apikey.FieldIPWhitelist:
return m.IPWhitelist()
case apikey.FieldIPBlacklist:
...
...
@@ -1045,6 +1100,8 @@ func (m *APIKeyMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldGroupID(ctx)
case apikey.FieldStatus:
return m.OldStatus(ctx)
case apikey.FieldLastUsedAt:
return m.OldLastUsedAt(ctx)
case apikey.FieldIPWhitelist:
return m.OldIPWhitelist(ctx)
case apikey.FieldIPBlacklist:
...
...
@@ -1120,6 +1177,13 @@ func (m *APIKeyMutation) SetField(name string, value ent.Value) error {
}
m.SetStatus(v)
return nil
case apikey.FieldLastUsedAt:
v, ok := value.(time.Time)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetLastUsedAt(v)
return nil
case apikey.FieldIPWhitelist:
v, ok := value.([]string)
if !ok {
...
...
@@ -1218,6 +1282,9 @@ func (m *APIKeyMutation) ClearedFields() []string {
if m.FieldCleared(apikey.FieldGroupID) {
fields = append(fields, apikey.FieldGroupID)
}
if m.FieldCleared(apikey.FieldLastUsedAt) {
fields = append(fields, apikey.FieldLastUsedAt)
}
if m.FieldCleared(apikey.FieldIPWhitelist) {
fields = append(fields, apikey.FieldIPWhitelist)
}
...
...
@@ -1247,6 +1314,9 @@ func (m *APIKeyMutation) ClearField(name string) error {
case apikey.FieldGroupID:
m.ClearGroupID()
return nil
case apikey.FieldLastUsedAt:
m.ClearLastUsedAt()
return nil
case apikey.FieldIPWhitelist:
m.ClearIPWhitelist()
return nil
...
...
@@ -1288,6 +1358,9 @@ func (m *APIKeyMutation) ResetField(name string) error {
case apikey.FieldStatus:
m.ResetStatus()
return nil
case apikey.FieldLastUsedAt:
m.ResetLastUsedAt()
return nil
case apikey.FieldIPWhitelist:
m.ResetIPWhitelist()
return nil
...
...
backend/ent/runtime/runtime.go
View file @
7be11952
...
...
@@ -94,11 +94,11 @@ func init() {
// apikey.StatusValidator is a validator for the "status" field. It is called by the builders before save.
apikey
.
StatusValidator
=
apikeyDescStatus
.
Validators
[
0
]
.
(
func
(
string
)
error
)
// apikeyDescQuota is the schema descriptor for quota field.
apikeyDescQuota
:=
apikeyFields
[
7
]
.
Descriptor
()
apikeyDescQuota
:=
apikeyFields
[
8
]
.
Descriptor
()
// apikey.DefaultQuota holds the default value on creation for the quota field.
apikey
.
DefaultQuota
=
apikeyDescQuota
.
Default
.
(
float64
)
// apikeyDescQuotaUsed is the schema descriptor for quota_used field.
apikeyDescQuotaUsed
:=
apikeyFields
[
8
]
.
Descriptor
()
apikeyDescQuotaUsed
:=
apikeyFields
[
9
]
.
Descriptor
()
// apikey.DefaultQuotaUsed holds the default value on creation for the quota_used field.
apikey
.
DefaultQuotaUsed
=
apikeyDescQuotaUsed
.
Default
.
(
float64
)
accountMixin
:=
schema
.
Account
{}
.
Mixin
()
...
...
backend/ent/schema/api_key.go
View file @
7be11952
...
...
@@ -47,6 +47,10 @@ func (APIKey) Fields() []ent.Field {
field
.
String
(
"status"
)
.
MaxLen
(
20
)
.
Default
(
domain
.
StatusActive
),
field
.
Time
(
"last_used_at"
)
.
Optional
()
.
Nillable
()
.
Comment
(
"Last usage time of this API key"
),
field
.
JSON
(
"ip_whitelist"
,
[]
string
{})
.
Optional
()
.
Comment
(
"Allowed IPs/CIDRs, e.g. [
\"
192.168.1.100
\"
,
\"
10.0.0.0/8
\"
]"
),
...
...
@@ -95,6 +99,7 @@ func (APIKey) Indexes() []ent.Index {
index
.
Fields
(
"group_id"
),
index
.
Fields
(
"status"
),
index
.
Fields
(
"deleted_at"
),
index
.
Fields
(
"last_used_at"
),
// Index for quota queries
index
.
Fields
(
"quota"
,
"quota_used"
),
index
.
Fields
(
"expires_at"
),
...
...
backend/go.sum
View file @
7be11952
...
...
@@ -176,6 +176,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
...
...
@@ -209,6 +211,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
...
...
@@ -238,6 +242,8 @@ github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkr
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
...
...
@@ -260,6 +266,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
...
...
backend/internal/handler/dto/api_key_mapper_last_used_test.go
0 → 100644
View file @
7be11952
package
dto
import
(
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
)
func
TestAPIKeyFromService_MapsLastUsedAt
(
t
*
testing
.
T
)
{
lastUsed
:=
time
.
Now
()
.
UTC
()
.
Truncate
(
time
.
Second
)
src
:=
&
service
.
APIKey
{
ID
:
1
,
UserID
:
2
,
Key
:
"sk-map-last-used"
,
Name
:
"Mapper"
,
Status
:
service
.
StatusActive
,
LastUsedAt
:
&
lastUsed
,
}
out
:=
APIKeyFromService
(
src
)
require
.
NotNil
(
t
,
out
)
require
.
NotNil
(
t
,
out
.
LastUsedAt
)
require
.
WithinDuration
(
t
,
lastUsed
,
*
out
.
LastUsedAt
,
time
.
Second
)
}
func
TestAPIKeyFromService_MapsNilLastUsedAt
(
t
*
testing
.
T
)
{
src
:=
&
service
.
APIKey
{
ID
:
1
,
UserID
:
2
,
Key
:
"sk-map-last-used-nil"
,
Name
:
"MapperNil"
,
Status
:
service
.
StatusActive
,
}
out
:=
APIKeyFromService
(
src
)
require
.
NotNil
(
t
,
out
)
require
.
Nil
(
t
,
out
.
LastUsedAt
)
}
backend/internal/handler/dto/mappers.go
View file @
7be11952
...
...
@@ -77,6 +77,7 @@ func APIKeyFromService(k *service.APIKey) *APIKey {
Status
:
k
.
Status
,
IPWhitelist
:
k
.
IPWhitelist
,
IPBlacklist
:
k
.
IPBlacklist
,
LastUsedAt
:
k
.
LastUsedAt
,
Quota
:
k
.
Quota
,
QuotaUsed
:
k
.
QuotaUsed
,
ExpiresAt
:
k
.
ExpiresAt
,
...
...
backend/internal/handler/dto/types.go
View file @
7be11952
...
...
@@ -38,6 +38,7 @@ type APIKey struct {
Status
string
`json:"status"`
IPWhitelist
[]
string
`json:"ip_whitelist"`
IPBlacklist
[]
string
`json:"ip_blacklist"`
LastUsedAt
*
time
.
Time
`json:"last_used_at"`
Quota
float64
`json:"quota"`
// Quota limit in USD (0 = unlimited)
QuotaUsed
float64
`json:"quota_used"`
// Used quota amount in USD
ExpiresAt
*
time
.
Time
`json:"expires_at"`
// Expiration time (nil = never expires)
...
...
backend/internal/repository/api_key_repo.go
View file @
7be11952
...
...
@@ -34,6 +34,7 @@ func (r *apiKeyRepository) Create(ctx context.Context, key *service.APIKey) erro
SetName
(
key
.
Name
)
.
SetStatus
(
key
.
Status
)
.
SetNillableGroupID
(
key
.
GroupID
)
.
SetNillableLastUsedAt
(
key
.
LastUsedAt
)
.
SetQuota
(
key
.
Quota
)
.
SetQuotaUsed
(
key
.
QuotaUsed
)
.
SetNillableExpiresAt
(
key
.
ExpiresAt
)
...
...
@@ -48,6 +49,7 @@ func (r *apiKeyRepository) Create(ctx context.Context, key *service.APIKey) erro
created
,
err
:=
builder
.
Save
(
ctx
)
if
err
==
nil
{
key
.
ID
=
created
.
ID
key
.
LastUsedAt
=
created
.
LastUsedAt
key
.
CreatedAt
=
created
.
CreatedAt
key
.
UpdatedAt
=
created
.
UpdatedAt
}
...
...
@@ -394,6 +396,21 @@ func (r *apiKeyRepository) IncrementQuotaUsed(ctx context.Context, id int64, amo
return
updated
.
QuotaUsed
,
nil
}
func
(
r
*
apiKeyRepository
)
UpdateLastUsed
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
affected
,
err
:=
r
.
client
.
APIKey
.
Update
()
.
Where
(
apikey
.
IDEQ
(
id
),
apikey
.
DeletedAtIsNil
())
.
SetLastUsedAt
(
usedAt
)
.
SetUpdatedAt
(
usedAt
)
.
Save
(
ctx
)
if
err
!=
nil
{
return
err
}
if
affected
==
0
{
return
service
.
ErrAPIKeyNotFound
}
return
nil
}
func
apiKeyEntityToService
(
m
*
dbent
.
APIKey
)
*
service
.
APIKey
{
if
m
==
nil
{
return
nil
...
...
@@ -406,6 +423,7 @@ func apiKeyEntityToService(m *dbent.APIKey) *service.APIKey {
Status
:
m
.
Status
,
IPWhitelist
:
m
.
IPWhitelist
,
IPBlacklist
:
m
.
IPBlacklist
,
LastUsedAt
:
m
.
LastUsedAt
,
CreatedAt
:
m
.
CreatedAt
,
UpdatedAt
:
m
.
UpdatedAt
,
GroupID
:
m
.
GroupID
,
...
...
backend/internal/repository/api_key_repo_last_used_unit_test.go
0 → 100644
View file @
7be11952
package
repository
import
(
"context"
"database/sql"
"testing"
"time"
dbent
"github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/enttest"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
"entgo.io/ent/dialect"
entsql
"entgo.io/ent/dialect/sql"
_
"modernc.org/sqlite"
)
func
newAPIKeyRepoSQLite
(
t
*
testing
.
T
)
(
*
apiKeyRepository
,
*
dbent
.
Client
)
{
t
.
Helper
()
db
,
err
:=
sql
.
Open
(
"sqlite"
,
"file:api_key_repo_last_used?mode=memory&cache=shared"
)
require
.
NoError
(
t
,
err
)
t
.
Cleanup
(
func
()
{
_
=
db
.
Close
()
})
_
,
err
=
db
.
Exec
(
"PRAGMA foreign_keys = ON"
)
require
.
NoError
(
t
,
err
)
drv
:=
entsql
.
OpenDB
(
dialect
.
SQLite
,
db
)
client
:=
enttest
.
NewClient
(
t
,
enttest
.
WithOptions
(
dbent
.
Driver
(
drv
)))
t
.
Cleanup
(
func
()
{
_
=
client
.
Close
()
})
return
&
apiKeyRepository
{
client
:
client
},
client
}
func
mustCreateAPIKeyRepoUser
(
t
*
testing
.
T
,
ctx
context
.
Context
,
client
*
dbent
.
Client
,
email
string
)
*
service
.
User
{
t
.
Helper
()
u
,
err
:=
client
.
User
.
Create
()
.
SetEmail
(
email
)
.
SetPasswordHash
(
"test-password-hash"
)
.
SetRole
(
service
.
RoleUser
)
.
SetStatus
(
service
.
StatusActive
)
.
Save
(
ctx
)
require
.
NoError
(
t
,
err
)
return
userEntityToService
(
u
)
}
func
TestAPIKeyRepository_CreateWithLastUsedAt
(
t
*
testing
.
T
)
{
repo
,
client
:=
newAPIKeyRepoSQLite
(
t
)
ctx
:=
context
.
Background
()
user
:=
mustCreateAPIKeyRepoUser
(
t
,
ctx
,
client
,
"create-last-used@test.com"
)
lastUsed
:=
time
.
Now
()
.
UTC
()
.
Add
(
-
time
.
Hour
)
.
Truncate
(
time
.
Second
)
key
:=
&
service
.
APIKey
{
UserID
:
user
.
ID
,
Key
:
"sk-create-last-used"
,
Name
:
"CreateWithLastUsed"
,
Status
:
service
.
StatusActive
,
LastUsedAt
:
&
lastUsed
,
}
require
.
NoError
(
t
,
repo
.
Create
(
ctx
,
key
))
require
.
NotNil
(
t
,
key
.
LastUsedAt
)
require
.
WithinDuration
(
t
,
lastUsed
,
*
key
.
LastUsedAt
,
time
.
Second
)
got
,
err
:=
repo
.
GetByID
(
ctx
,
key
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
got
.
LastUsedAt
)
require
.
WithinDuration
(
t
,
lastUsed
,
*
got
.
LastUsedAt
,
time
.
Second
)
}
func
TestAPIKeyRepository_UpdateLastUsed
(
t
*
testing
.
T
)
{
repo
,
client
:=
newAPIKeyRepoSQLite
(
t
)
ctx
:=
context
.
Background
()
user
:=
mustCreateAPIKeyRepoUser
(
t
,
ctx
,
client
,
"update-last-used@test.com"
)
key
:=
&
service
.
APIKey
{
UserID
:
user
.
ID
,
Key
:
"sk-update-last-used"
,
Name
:
"UpdateLastUsed"
,
Status
:
service
.
StatusActive
,
}
require
.
NoError
(
t
,
repo
.
Create
(
ctx
,
key
))
before
,
err
:=
repo
.
GetByID
(
ctx
,
key
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
before
.
LastUsedAt
)
target
:=
time
.
Now
()
.
UTC
()
.
Add
(
2
*
time
.
Minute
)
.
Truncate
(
time
.
Second
)
require
.
NoError
(
t
,
repo
.
UpdateLastUsed
(
ctx
,
key
.
ID
,
target
))
after
,
err
:=
repo
.
GetByID
(
ctx
,
key
.
ID
)
require
.
NoError
(
t
,
err
)
require
.
NotNil
(
t
,
after
.
LastUsedAt
)
require
.
WithinDuration
(
t
,
target
,
*
after
.
LastUsedAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
target
,
after
.
UpdatedAt
,
time
.
Second
)
}
func
TestAPIKeyRepository_UpdateLastUsedDeletedKey
(
t
*
testing
.
T
)
{
repo
,
client
:=
newAPIKeyRepoSQLite
(
t
)
ctx
:=
context
.
Background
()
user
:=
mustCreateAPIKeyRepoUser
(
t
,
ctx
,
client
,
"deleted-last-used@test.com"
)
key
:=
&
service
.
APIKey
{
UserID
:
user
.
ID
,
Key
:
"sk-update-last-used-deleted"
,
Name
:
"UpdateLastUsedDeleted"
,
Status
:
service
.
StatusActive
,
}
require
.
NoError
(
t
,
repo
.
Create
(
ctx
,
key
))
require
.
NoError
(
t
,
repo
.
Delete
(
ctx
,
key
.
ID
))
err
:=
repo
.
UpdateLastUsed
(
ctx
,
key
.
ID
,
time
.
Now
()
.
UTC
())
require
.
ErrorIs
(
t
,
err
,
service
.
ErrAPIKeyNotFound
)
}
func
TestAPIKeyRepository_UpdateLastUsedDBError
(
t
*
testing
.
T
)
{
repo
,
client
:=
newAPIKeyRepoSQLite
(
t
)
ctx
:=
context
.
Background
()
user
:=
mustCreateAPIKeyRepoUser
(
t
,
ctx
,
client
,
"db-error-last-used@test.com"
)
key
:=
&
service
.
APIKey
{
UserID
:
user
.
ID
,
Key
:
"sk-update-last-used-db-error"
,
Name
:
"UpdateLastUsedDBError"
,
Status
:
service
.
StatusActive
,
}
require
.
NoError
(
t
,
repo
.
Create
(
ctx
,
key
))
require
.
NoError
(
t
,
client
.
Close
())
err
:=
repo
.
UpdateLastUsed
(
ctx
,
key
.
ID
,
time
.
Now
()
.
UTC
())
require
.
Error
(
t
,
err
)
}
func
TestAPIKeyRepository_CreateDuplicateKey
(
t
*
testing
.
T
)
{
repo
,
client
:=
newAPIKeyRepoSQLite
(
t
)
ctx
:=
context
.
Background
()
user
:=
mustCreateAPIKeyRepoUser
(
t
,
ctx
,
client
,
"duplicate-key@test.com"
)
first
:=
&
service
.
APIKey
{
UserID
:
user
.
ID
,
Key
:
"sk-duplicate"
,
Name
:
"first"
,
Status
:
service
.
StatusActive
,
}
second
:=
&
service
.
APIKey
{
UserID
:
user
.
ID
,
Key
:
"sk-duplicate"
,
Name
:
"second"
,
Status
:
service
.
StatusActive
,
}
require
.
NoError
(
t
,
repo
.
Create
(
ctx
,
first
))
err
:=
repo
.
Create
(
ctx
,
second
)
require
.
ErrorIs
(
t
,
err
,
service
.
ErrAPIKeyExists
)
}
backend/internal/server/api_contract_test.go
View file @
7be11952
...
...
@@ -83,6 +83,7 @@ func TestAPIContracts(t *testing.T) {
"status": "active",
"ip_whitelist": null,
"ip_blacklist": null,
"last_used_at": null,
"quota": 0,
"quota_used": 0,
"expires_at": null,
...
...
@@ -122,6 +123,7 @@ func TestAPIContracts(t *testing.T) {
"status": "active",
"ip_whitelist": null,
"ip_blacklist": null,
"last_used_at": null,
"quota": 0,
"quota_used": 0,
"expires_at": null,
...
...
@@ -1471,6 +1473,20 @@ func (r *stubApiKeyRepo) IncrementQuotaUsed(ctx context.Context, id int64, amoun
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
r
*
stubApiKeyRepo
)
UpdateLastUsed
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
key
,
ok
:=
r
.
byID
[
id
]
if
!
ok
{
return
service
.
ErrAPIKeyNotFound
}
ts
:=
usedAt
key
.
LastUsedAt
=
&
ts
key
.
UpdatedAt
=
usedAt
clone
:=
*
key
r
.
byID
[
id
]
=
&
clone
r
.
byKey
[
clone
.
Key
]
=
&
clone
return
nil
}
type
stubUsageLogRepo
struct
{
userLogs
map
[
int64
][]
service
.
UsageLog
}
...
...
backend/internal/server/middleware/api_key_auth.go
View file @
7be11952
...
...
@@ -125,6 +125,7 @@ func apiKeyAuthWithSubscription(apiKeyService *service.APIKeyService, subscripti
})
c
.
Set
(
string
(
ContextKeyUserRole
),
apiKey
.
User
.
Role
)
setGroupContext
(
c
,
apiKey
.
Group
)
_
=
apiKeyService
.
TouchLastUsed
(
c
.
Request
.
Context
(),
apiKey
.
ID
)
c
.
Next
()
return
}
...
...
@@ -184,6 +185,7 @@ func apiKeyAuthWithSubscription(apiKeyService *service.APIKeyService, subscripti
})
c
.
Set
(
string
(
ContextKeyUserRole
),
apiKey
.
User
.
Role
)
setGroupContext
(
c
,
apiKey
.
Group
)
_
=
apiKeyService
.
TouchLastUsed
(
c
.
Request
.
Context
(),
apiKey
.
ID
)
c
.
Next
()
}
...
...
backend/internal/server/middleware/api_key_auth_google.go
View file @
7be11952
...
...
@@ -64,6 +64,7 @@ func APIKeyAuthWithSubscriptionGoogle(apiKeyService *service.APIKeyService, subs
})
c
.
Set
(
string
(
ContextKeyUserRole
),
apiKey
.
User
.
Role
)
setGroupContext
(
c
,
apiKey
.
Group
)
_
=
apiKeyService
.
TouchLastUsed
(
c
.
Request
.
Context
(),
apiKey
.
ID
)
c
.
Next
()
return
}
...
...
@@ -104,6 +105,7 @@ func APIKeyAuthWithSubscriptionGoogle(apiKeyService *service.APIKeyService, subs
})
c
.
Set
(
string
(
ContextKeyUserRole
),
apiKey
.
User
.
Role
)
setGroupContext
(
c
,
apiKey
.
Group
)
_
=
apiKeyService
.
TouchLastUsed
(
c
.
Request
.
Context
(),
apiKey
.
ID
)
c
.
Next
()
}
}
...
...
backend/internal/server/middleware/api_key_auth_google_test.go
View file @
7be11952
...
...
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
...
...
@@ -18,7 +19,8 @@ import (
)
type
fakeAPIKeyRepo
struct
{
getByKey
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
getByKey
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
updateLastUsed
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
}
func
(
f
fakeAPIKeyRepo
)
Create
(
ctx
context
.
Context
,
key
*
service
.
APIKey
)
error
{
...
...
@@ -78,6 +80,12 @@ func (f fakeAPIKeyRepo) ListKeysByGroupID(ctx context.Context, groupID int64) ([
func
(
f
fakeAPIKeyRepo
)
IncrementQuotaUsed
(
ctx
context
.
Context
,
id
int64
,
amount
float64
)
(
float64
,
error
)
{
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
f
fakeAPIKeyRepo
)
UpdateLastUsed
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
if
f
.
updateLastUsed
!=
nil
{
return
f
.
updateLastUsed
(
ctx
,
id
,
usedAt
)
}
return
nil
}
type
googleErrorResponse
struct
{
Error
struct
{
...
...
@@ -356,3 +364,144 @@ func TestApiKeyAuthWithSubscriptionGoogle_InsufficientBalance(t *testing.T) {
require
.
Equal
(
t
,
"Insufficient account balance"
,
resp
.
Error
.
Message
)
require
.
Equal
(
t
,
"PERMISSION_DENIED"
,
resp
.
Error
.
Status
)
}
func
TestApiKeyAuthWithSubscriptionGoogle_TouchesLastUsedOnSuccess
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
user
:=
&
service
.
User
{
ID
:
11
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
10
,
Concurrency
:
3
,
}
apiKey
:=
&
service
.
APIKey
{
ID
:
201
,
UserID
:
user
.
ID
,
Key
:
"google-touch-ok"
,
Status
:
service
.
StatusActive
,
User
:
user
,
}
var
touchedID
int64
var
touchedAt
time
.
Time
r
:=
gin
.
New
()
apiKeyService
:=
newTestAPIKeyService
(
fakeAPIKeyRepo
{
getByKey
:
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
{
if
key
!=
apiKey
.
Key
{
return
nil
,
service
.
ErrAPIKeyNotFound
}
clone
:=
*
apiKey
return
&
clone
,
nil
},
updateLastUsed
:
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
touchedID
=
id
touchedAt
=
usedAt
return
nil
},
})
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeSimple
}
r
.
Use
(
APIKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
cfg
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
.
Header
.
Set
(
"x-goog-api-key"
,
apiKey
.
Key
)
rec
:=
httptest
.
NewRecorder
()
r
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
apiKey
.
ID
,
touchedID
)
require
.
False
(
t
,
touchedAt
.
IsZero
())
}
func
TestApiKeyAuthWithSubscriptionGoogle_TouchFailureDoesNotBlock
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
user
:=
&
service
.
User
{
ID
:
12
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
10
,
Concurrency
:
3
,
}
apiKey
:=
&
service
.
APIKey
{
ID
:
202
,
UserID
:
user
.
ID
,
Key
:
"google-touch-fail"
,
Status
:
service
.
StatusActive
,
User
:
user
,
}
touchCalls
:=
0
r
:=
gin
.
New
()
apiKeyService
:=
newTestAPIKeyService
(
fakeAPIKeyRepo
{
getByKey
:
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
{
if
key
!=
apiKey
.
Key
{
return
nil
,
service
.
ErrAPIKeyNotFound
}
clone
:=
*
apiKey
return
&
clone
,
nil
},
updateLastUsed
:
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
touchCalls
++
return
errors
.
New
(
"write failed"
)
},
})
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeSimple
}
r
.
Use
(
APIKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
cfg
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
.
Header
.
Set
(
"x-goog-api-key"
,
apiKey
.
Key
)
rec
:=
httptest
.
NewRecorder
()
r
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
1
,
touchCalls
)
}
func
TestApiKeyAuthWithSubscriptionGoogle_TouchesLastUsedInStandardMode
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
user
:=
&
service
.
User
{
ID
:
13
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
10
,
Concurrency
:
3
,
}
apiKey
:=
&
service
.
APIKey
{
ID
:
203
,
UserID
:
user
.
ID
,
Key
:
"google-touch-standard"
,
Status
:
service
.
StatusActive
,
User
:
user
,
}
touchCalls
:=
0
r
:=
gin
.
New
()
apiKeyService
:=
newTestAPIKeyService
(
fakeAPIKeyRepo
{
getByKey
:
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
{
if
key
!=
apiKey
.
Key
{
return
nil
,
service
.
ErrAPIKeyNotFound
}
clone
:=
*
apiKey
return
&
clone
,
nil
},
updateLastUsed
:
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
touchCalls
++
return
nil
},
})
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeStandard
}
r
.
Use
(
APIKeyAuthWithSubscriptionGoogle
(
apiKeyService
,
nil
,
cfg
))
r
.
GET
(
"/v1beta/test"
,
func
(
c
*
gin
.
Context
)
{
c
.
JSON
(
200
,
gin
.
H
{
"ok"
:
true
})
})
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/v1beta/test"
,
nil
)
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
apiKey
.
Key
)
rec
:=
httptest
.
NewRecorder
()
r
.
ServeHTTP
(
rec
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
rec
.
Code
)
require
.
Equal
(
t
,
1
,
touchCalls
)
}
backend/internal/server/middleware/api_key_auth_test.go
View file @
7be11952
...
...
@@ -351,6 +351,147 @@ func TestAPIKeyAuthIPRestrictionDoesNotTrustSpoofedForwardHeaders(t *testing.T)
require
.
Contains
(
t
,
w
.
Body
.
String
(),
"ACCESS_DENIED"
)
}
func
TestAPIKeyAuthTouchesLastUsedOnSuccess
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
user
:=
&
service
.
User
{
ID
:
7
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
10
,
Concurrency
:
3
,
}
apiKey
:=
&
service
.
APIKey
{
ID
:
100
,
UserID
:
user
.
ID
,
Key
:
"touch-ok"
,
Status
:
service
.
StatusActive
,
User
:
user
,
}
var
touchedID
int64
var
touchedAt
time
.
Time
apiKeyRepo
:=
&
stubApiKeyRepo
{
getByKey
:
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
{
if
key
!=
apiKey
.
Key
{
return
nil
,
service
.
ErrAPIKeyNotFound
}
clone
:=
*
apiKey
return
&
clone
,
nil
},
updateLastUsed
:
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
touchedID
=
id
touchedAt
=
usedAt
return
nil
},
}
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeSimple
}
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepo
,
nil
,
nil
,
nil
,
nil
,
nil
,
cfg
)
router
:=
newAuthTestRouter
(
apiKeyService
,
nil
,
cfg
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"x-api-key"
,
apiKey
.
Key
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
w
.
Code
)
require
.
Equal
(
t
,
apiKey
.
ID
,
touchedID
)
require
.
False
(
t
,
touchedAt
.
IsZero
(),
"expected touch timestamp"
)
}
func
TestAPIKeyAuthTouchLastUsedFailureDoesNotBlock
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
user
:=
&
service
.
User
{
ID
:
8
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
10
,
Concurrency
:
3
,
}
apiKey
:=
&
service
.
APIKey
{
ID
:
101
,
UserID
:
user
.
ID
,
Key
:
"touch-fail"
,
Status
:
service
.
StatusActive
,
User
:
user
,
}
touchCalls
:=
0
apiKeyRepo
:=
&
stubApiKeyRepo
{
getByKey
:
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
{
if
key
!=
apiKey
.
Key
{
return
nil
,
service
.
ErrAPIKeyNotFound
}
clone
:=
*
apiKey
return
&
clone
,
nil
},
updateLastUsed
:
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
touchCalls
++
return
errors
.
New
(
"db unavailable"
)
},
}
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeSimple
}
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepo
,
nil
,
nil
,
nil
,
nil
,
nil
,
cfg
)
router
:=
newAuthTestRouter
(
apiKeyService
,
nil
,
cfg
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"x-api-key"
,
apiKey
.
Key
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
w
.
Code
,
"touch failure should not block request"
)
require
.
Equal
(
t
,
1
,
touchCalls
)
}
func
TestAPIKeyAuthTouchesLastUsedInStandardMode
(
t
*
testing
.
T
)
{
gin
.
SetMode
(
gin
.
TestMode
)
user
:=
&
service
.
User
{
ID
:
9
,
Role
:
service
.
RoleUser
,
Status
:
service
.
StatusActive
,
Balance
:
10
,
Concurrency
:
3
,
}
apiKey
:=
&
service
.
APIKey
{
ID
:
102
,
UserID
:
user
.
ID
,
Key
:
"touch-standard"
,
Status
:
service
.
StatusActive
,
User
:
user
,
}
touchCalls
:=
0
apiKeyRepo
:=
&
stubApiKeyRepo
{
getByKey
:
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
{
if
key
!=
apiKey
.
Key
{
return
nil
,
service
.
ErrAPIKeyNotFound
}
clone
:=
*
apiKey
return
&
clone
,
nil
},
updateLastUsed
:
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
touchCalls
++
return
nil
},
}
cfg
:=
&
config
.
Config
{
RunMode
:
config
.
RunModeStandard
}
apiKeyService
:=
service
.
NewAPIKeyService
(
apiKeyRepo
,
nil
,
nil
,
nil
,
nil
,
nil
,
cfg
)
router
:=
newAuthTestRouter
(
apiKeyService
,
nil
,
cfg
)
w
:=
httptest
.
NewRecorder
()
req
:=
httptest
.
NewRequest
(
http
.
MethodGet
,
"/t"
,
nil
)
req
.
Header
.
Set
(
"x-api-key"
,
apiKey
.
Key
)
router
.
ServeHTTP
(
w
,
req
)
require
.
Equal
(
t
,
http
.
StatusOK
,
w
.
Code
)
require
.
Equal
(
t
,
1
,
touchCalls
)
}
func
newAuthTestRouter
(
apiKeyService
*
service
.
APIKeyService
,
subscriptionService
*
service
.
SubscriptionService
,
cfg
*
config
.
Config
)
*
gin
.
Engine
{
router
:=
gin
.
New
()
router
.
Use
(
gin
.
HandlerFunc
(
NewAPIKeyAuthMiddleware
(
apiKeyService
,
subscriptionService
,
cfg
)))
...
...
@@ -361,7 +502,8 @@ func newAuthTestRouter(apiKeyService *service.APIKeyService, subscriptionService
}
type
stubApiKeyRepo
struct
{
getByKey
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
getByKey
func
(
ctx
context
.
Context
,
key
string
)
(
*
service
.
APIKey
,
error
)
updateLastUsed
func
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
}
func
(
r
*
stubApiKeyRepo
)
Create
(
ctx
context
.
Context
,
key
*
service
.
APIKey
)
error
{
...
...
@@ -439,6 +581,13 @@ func (r *stubApiKeyRepo) IncrementQuotaUsed(ctx context.Context, id int64, amoun
return
0
,
errors
.
New
(
"not implemented"
)
}
func
(
r
*
stubApiKeyRepo
)
UpdateLastUsed
(
ctx
context
.
Context
,
id
int64
,
usedAt
time
.
Time
)
error
{
if
r
.
updateLastUsed
!=
nil
{
return
r
.
updateLastUsed
(
ctx
,
id
,
usedAt
)
}
return
nil
}
type
stubUserSubscriptionRepo
struct
{
getActive
func
(
ctx
context
.
Context
,
userID
,
groupID
int64
)
(
*
service
.
UserSubscription
,
error
)
updateStatus
func
(
ctx
context
.
Context
,
subscriptionID
int64
,
status
string
)
error
...
...
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment