Accounts with limits: a Newznab key and a tier per user, daily API and NZB counts, invites
ACCESS=accounts (config/access.ts; open stays the default and behaves as before): Newznab and the catalog's releases routes name a user (apikey or the web interface's token), whose tier sets searches and NZBs per UTC day (Newznab 500/501, the catalog 429) and how far back they see (102/403). Counted in one upsert per use, so the last one can't be taken twice; caps, stats and refused requests aren't counted. Signup takes an invite (SIGNUP), which may give a tier; ace access:invite, access:tier (extends a running tier), access:suspend. The web interface signs in and shows the day's usage and the Newznab key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c55918350e
commit
1322e42fb7
23 files changed
+1023
-39
No files matched your search
@@ -0,0 +1,49 @@
|
||||
import { BaseSchema } from '@adonisjs/lucid/schema'
|
||||
|
||||
/**
|
||||
* Accounts with limits (config/access.ts): a Newznab key and a tier per user,
|
||||
* invites, and what each user used per day.
|
||||
*/
|
||||
export default class extends BaseSchema {
|
||||
async up() {
|
||||
this.schema.alterTable('users', (table) => {
|
||||
// Existing users get a key of their own (a volatile default is evaluated per row).
|
||||
table.string('api_key', 32).notNullable().unique().defaultTo(this.raw(`md5(random()::text || clock_timestamp()::text)`))
|
||||
table.string('tier', 16).notNullable().defaultTo('free')
|
||||
// Null: the tier doesn't run out.
|
||||
table.timestamp('tier_until', { useTz: true }).nullable()
|
||||
table.boolean('suspended').notNullable().defaultTo(false)
|
||||
})
|
||||
|
||||
this.schema.createTable('invites', (table) => {
|
||||
table.string('code', 32).primary()
|
||||
// What the invite gives (e.g. a sold VIP invite); null: the free tier.
|
||||
table.string('tier', 16).nullable()
|
||||
table.integer('tier_days').nullable()
|
||||
table.string('note').nullable()
|
||||
table.integer('used_by').nullable().references('id').inTable('users').onDelete('SET NULL')
|
||||
table.timestamp('used_at', { useTz: true }).nullable()
|
||||
table.timestamp('created_at', { useTz: true }).notNullable().defaultTo(this.now())
|
||||
})
|
||||
|
||||
this.schema.createTable('usage', (table) => {
|
||||
table.integer('user_id').notNullable().references('id').inTable('users').onDelete('CASCADE')
|
||||
// A UTC day.
|
||||
table.date('day').notNullable()
|
||||
table.integer('api_hits').notNullable().defaultTo(0)
|
||||
table.integer('grabs').notNullable().defaultTo(0)
|
||||
table.primary(['user_id', 'day'])
|
||||
})
|
||||
}
|
||||
|
||||
async down() {
|
||||
this.schema.dropTable('usage')
|
||||
this.schema.dropTable('invites')
|
||||
this.schema.alterTable('users', (table) => {
|
||||
table.dropColumn('api_key')
|
||||
table.dropColumn('tier')
|
||||
table.dropColumn('tier_until')
|
||||
table.dropColumn('suspended')
|
||||
})
|
||||
}
|
||||
}
|
||||
+41
-1
@@ -87,6 +87,25 @@ export class GroupSchema extends BaseModel {
|
||||
declare tail: boolean
|
||||
}
|
||||
|
||||
export class InviteSchema extends BaseModel {
|
||||
static $columns = ['code', 'createdAt', 'note', 'tier', 'tierDays', 'usedAt', 'usedBy'] as const
|
||||
$columns = InviteSchema.$columns
|
||||
@column({ isPrimary: true })
|
||||
declare code: string
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column()
|
||||
declare note: string | null
|
||||
@column()
|
||||
declare tier: string | null
|
||||
@column()
|
||||
declare tierDays: number | null
|
||||
@column.dateTime()
|
||||
declare usedAt: DateTime | null
|
||||
@column()
|
||||
declare usedBy: number | null
|
||||
}
|
||||
|
||||
export class NodeAccessTokenSchema extends BaseModel {
|
||||
static $columns = ['abilities', 'createdAt', 'expiresAt', 'hash', 'id', 'lastUsedAt', 'name', 'tokenableId', 'type', 'updatedAt'] as const
|
||||
$columns = NodeAccessTokenSchema.$columns
|
||||
@@ -399,9 +418,24 @@ export class RuleSetSchema extends BaseModel {
|
||||
declare version: number
|
||||
}
|
||||
|
||||
export class UsageSchema extends BaseModel {
|
||||
static $columns = ['apiHits', 'day', 'grabs', 'userId'] as const
|
||||
$columns = UsageSchema.$columns
|
||||
@column()
|
||||
declare apiHits: number
|
||||
@column.date()
|
||||
declare day: DateTime
|
||||
@column()
|
||||
declare grabs: number
|
||||
@column({ isPrimary: true })
|
||||
declare userId: number
|
||||
}
|
||||
|
||||
export class UserSchema extends BaseModel {
|
||||
static $columns = ['createdAt', 'email', 'fullName', 'id', 'password', 'updatedAt'] as const
|
||||
static $columns = ['apiKey', 'createdAt', 'email', 'fullName', 'id', 'password', 'suspended', 'tier', 'tierUntil', 'updatedAt'] as const
|
||||
$columns = UserSchema.$columns
|
||||
@column()
|
||||
declare apiKey: string
|
||||
@column.dateTime({ autoCreate: true })
|
||||
declare createdAt: DateTime
|
||||
@column()
|
||||
@@ -412,6 +446,12 @@ export class UserSchema extends BaseModel {
|
||||
declare id: number
|
||||
@column({ serializeAs: null })
|
||||
declare password: string
|
||||
@column()
|
||||
declare suspended: boolean
|
||||
@column()
|
||||
declare tier: string
|
||||
@column.dateTime()
|
||||
declare tierUntil: DateTime | null
|
||||
@column.dateTime({ autoCreate: true, autoUpdate: true })
|
||||
declare updatedAt: DateTime | null
|
||||
}
|
||||
Reference in new issue
Block a user