Files
usenet-indexer/database/migrations/1770000003300_access.ts
Daan MeijerandClaude Opus 5.5 1322e42fb7
Test and deploy / test (push) Successful in 47s
Test and deploy / edge (push) Successful in 1m44s
Test and deploy / deploy (push) Skipped
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>
2026-10-17 00:16:45 +02:00

50 lines
1.9 KiB
TypeScript

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')
})
}
}