Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE `license` ADD COLUMN `ip` VARCHAR(191) NULL,
ADD COLUMN `ipBoundEnabled` BOOLEAN NOT NULL DEFAULT false;

-- AlterTable
ALTER TABLE `log` MODIFY `result` ENUM('VALID', 'NOT_FOUND', 'NOT_ACTIVE', 'EXPIRED', 'LICENSE_SCOPE_FAILED', 'IP_LIMIT_EXCEEDED', 'RATE_LIMIT_EXCEEDED', 'BOUND_IP_MISMATCH') NOT NULL;
4 changes: 4 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ model License {
replenishAmount Int?
replenishInterval ReplenishInterval?

ip String?
ipBoundEnabled Boolean @default(false)

createdAt DateTime @default(now())

logs Log[]
Expand All @@ -72,6 +75,7 @@ enum ValidationResult {
LICENSE_SCOPE_FAILED
IP_LIMIT_EXCEEDED
RATE_LIMIT_EXCEEDED
BOUND_IP_MISMATCH
}

model Log {
Expand Down
18 changes: 18 additions & 0 deletions backend/src/controller/verify-license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,24 @@ async function checkLicense(
}
}

if (license.ipBoundEnabled) {
if (license.ip == null) {
license.ip = ip;

// Save the updated license to the database
await prisma.license.update({
where: { id: license.id },
data: { ip: license.ip },
});

return "VALID";
}

if (license.ip != ip) {
return "BOUND_IP_MISMATCH";
}
}

return "VALID";
}

Expand Down
2 changes: 2 additions & 0 deletions backend/src/routers/license-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const licenseCreateSchema = z.object({
active: z.boolean(),

ipLimit: z.number().int().nullable(),
ipBoundEnabled: z.boolean().default(false),
ip: z.string().nullable().optional(),
licenseScope: z.string().nullable(),
expirationDate: z.date().nullable(),

Expand Down
1 change: 1 addition & 0 deletions backend/src/routers/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const validationResultSchema = z.enum([
"LICENSE_SCOPE_FAILED",
"IP_LIMIT_EXCEEDED",
"RATE_LIMIT_EXCEEDED",
"BOUND_IP_MISMATCH"
]);

export const logsRouter = router({
Expand Down
4 changes: 3 additions & 1 deletion backend/src/routers/public/license-verify.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const verifyLicenseSchema = z
* - `LICENSE_SCOPE_FAILED`: The scope of the license does not match the scope in the request
* - `IP_LIMIT_EXCEEDED`: The IP limit has been exceeded
* - `RATE_LIMIT_EXCEEDED`: The rate limit has been exceeded
* - 'BOUND_IP_MISMATCH': The request was made from an IP address that does not match the one bound to the license
* @example "VALID"
*/
type ValidationResult =
Expand All @@ -51,7 +52,8 @@ type ValidationResult =
| "EXPIRED"
| "LICENSE_SCOPE_FAILED"
| "IP_LIMIT_EXCEEDED"
| "RATE_LIMIT_EXCEEDED";
| "RATE_LIMIT_EXCEEDED"
| "BOUND_IP_MISMATCH";

interface ValidationResponse {
/**
Expand Down
13 changes: 12 additions & 1 deletion backend/src/routers/public/license.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ interface License {
*/
ipLimit: number | null;

/**
* Whether to bind this license to the first IP that uses it.
*/
ipBoundEnabled: boolean | false;

/**
* The first IP that used this license.
* @default null
*/
ip?: string | null;

/**
* Scope of the license.
* See https://docs.licensegate.io/restriction-options/scope
Expand Down Expand Up @@ -143,7 +154,7 @@ interface LicenseUpdateInput

@Route("admin/licenses")
@Tags("Admin")
export class LicenseController extends Controller {
export class LicenseController extends Controller {
licenseService!: LicenseService;

constructor() {
Expand Down
8 changes: 7 additions & 1 deletion backend/src/tsoa-generated/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const models: TsoaRoute.Models = {
"name": {"dataType":"string","required":true},
"notes": {"dataType":"string","required":true},
"ipLimit": {"dataType":"union","subSchemas":[{"dataType":"integer"},{"dataType":"enum","enums":[null]}],"default":null,"required":true},
"ipBoundEnabled": {"dataType":"union","subSchemas":[{"dataType":"boolean"},{"dataType":"enum","enums":[false]}],"required":true},
"ip": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"default":null},
"licenseScope": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"enum","enums":[null]}],"default":null,"required":true},
"expirationDate": {"dataType":"union","subSchemas":[{"dataType":"datetime"},{"dataType":"enum","enums":[null]}],"default":null,"required":true},
"validationPoints": {"dataType":"union","subSchemas":[{"dataType":"double"},{"dataType":"enum","enums":[null]}],"default":null,"required":true},
Expand Down Expand Up @@ -76,6 +78,8 @@ const models: TsoaRoute.Models = {
"name": {"dataType":"string","required":true},
"notes": {"dataType":"string","required":true},
"ipLimit": {"dataType":"integer","default":null,"required":true},
"ipBoundEnabled": {"dataType":"boolean","required":true},
"ip": {"dataType":"string","default":null},
"licenseScope": {"dataType":"string","default":null,"required":true},
"expirationDate": {"dataType":"datetime","default":null,"required":true},
"validationPoints": {"dataType":"double","default":null,"required":true},
Expand Down Expand Up @@ -104,6 +108,8 @@ const models: TsoaRoute.Models = {
"name": {"dataType":"string"},
"notes": {"dataType":"string"},
"ipLimit": {"dataType":"integer","default":null},
"ipBoundEnabled": {"dataType":"boolean"},
"ip": {"dataType":"string","default":null},
"licenseScope": {"dataType":"string","default":null},
"expirationDate": {"dataType":"datetime","default":null},
"validationPoints": {"dataType":"double","default":null},
Expand All @@ -116,7 +122,7 @@ const models: TsoaRoute.Models = {
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"ValidationResult": {
"dataType": "refAlias",
"type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["VALID"]},{"dataType":"enum","enums":["NOT_FOUND"]},{"dataType":"enum","enums":["NOT_ACTIVE"]},{"dataType":"enum","enums":["EXPIRED"]},{"dataType":"enum","enums":["LICENSE_SCOPE_FAILED"]},{"dataType":"enum","enums":["IP_LIMIT_EXCEEDED"]},{"dataType":"enum","enums":["RATE_LIMIT_EXCEEDED"]}],"validators":{}},
"type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["VALID"]},{"dataType":"enum","enums":["NOT_FOUND"]},{"dataType":"enum","enums":["NOT_ACTIVE"]},{"dataType":"enum","enums":["EXPIRED"]},{"dataType":"enum","enums":["LICENSE_SCOPE_FAILED"]},{"dataType":"enum","enums":["IP_LIMIT_EXCEEDED"]},{"dataType":"enum","enums":["RATE_LIMIT_EXCEEDED"]},{"dataType":"enum","enums":["BOUND_IP_MISMATCH"]}],"validators":{}},
},
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
"ValidationResponse": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts">
import type { CreateLicense } from '../../../../trpcClient'

export let entity: CreateLicense
</script>

<div class="flex items-center gap-2">
<span class="text-sm font-semibold">IP Bound enabled:</span>
<input type="checkbox" bind:checked={entity.ipBoundEnabled} />
</div>

{#if entity.ipBoundEnabled == true}
<div
class="mt-2 text-xs text-orange-500 underline cursor-pointer"
on:click={() => (entity.ip = null)}
>
Reset IP address
</div>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Collapsable from '../../../basics/Collapsable.svelte'
import OnOffChip from '../OnOffChip.svelte'
import Expiration from './Expiration.svelte'
import IpBound from './IpBound.svelte'
import IpLimit from './IpLimit.svelte'
import RateLimit from './RateLimit.svelte'
import Scope from './Scope.svelte'
Expand All @@ -17,6 +18,13 @@
component: IpLimit,
active: entity.ipLimit !== null,
},
{
name: 'IP Bound',
description:
'You can bound an IP adress to the License, IP will be automatically bound on first verify.',
component: IpBound,
active: entity.ipBoundEnabled !== false,
},

{
name: 'Rate Limit',
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/lib/components/license/LicenseLimitInfo.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
active: license.ipLimit !== null,
},

{
name: 'IP Bound',
text: `The license is bound to the first IP address that uses it.`,
active: license.ipBoundEnabled !== false,
},

{
name: 'Rate Limit',
text: `Limited to ${license.replenishAmount} verifications every ${
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/components/logs/FetchingLogsTable.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
'LICENSE_SCOPE_FAILED',
'IP_LIMIT_EXCEEDED',
'RATE_LIMIT_EXCEEDED',
'BOUND_IP_MISMATCH'
] satisfies ValidationResult[]
</script>

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/routes/(app)/licenses/new/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
active: true,

ipLimit: null,
ipBoundEnabled: false,
ip: null,
licenseScope: null,
expirationDate: null,

Expand Down
45 changes: 43 additions & 2 deletions open-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,26 @@
"description": "Limit of IPs that can validate this license.\r\nSee https://docs.licensegate.io/restriction-options/ip-limit",
"default": null
},
"ipBoundEnabled": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "boolean",
"enum": [
false
]
}
],
"description": "Whether to bind this license to the first IP that uses it."
},
"ip": {
"type": "string",
"nullable": true,
"description": "The first IP that used this license.",
"default": null
},
"licenseScope": {
"type": "string",
"nullable": true,
Expand Down Expand Up @@ -110,6 +130,7 @@
"name",
"notes",
"ipLimit",
"ipBoundEnabled",
"licenseScope",
"expirationDate",
"validationPoints",
Expand Down Expand Up @@ -202,6 +223,15 @@
"description": "Limit of IPs that can validate this license.\r\nSee https://docs.licensegate.io/restriction-options/ip-limit",
"default": null
},
"ipBoundEnabled": {
"type": "boolean",
"description": "Whether to bind this license to the first IP that uses it."
},
"ip": {
"type": "string",
"description": "The first IP that used this license.",
"default": null
},
"licenseScope": {
"type": "string",
"description": "Scope of the license.\r\nSee https://docs.licensegate.io/restriction-options/scope",
Expand Down Expand Up @@ -245,6 +275,7 @@
"name",
"notes",
"ipLimit",
"ipBoundEnabled",
"licenseScope",
"expirationDate",
"validationPoints",
Expand Down Expand Up @@ -300,6 +331,15 @@
"description": "Limit of IPs that can validate this license.\r\nSee https://docs.licensegate.io/restriction-options/ip-limit",
"default": null
},
"ipBoundEnabled": {
"type": "boolean",
"description": "Whether to bind this license to the first IP that uses it."
},
"ip": {
"type": "string",
"description": "The first IP that used this license.",
"default": null
},
"licenseScope": {
"type": "string",
"description": "Scope of the license.\r\nSee https://docs.licensegate.io/restriction-options/scope",
Expand Down Expand Up @@ -346,10 +386,11 @@
"EXPIRED",
"LICENSE_SCOPE_FAILED",
"IP_LIMIT_EXCEEDED",
"RATE_LIMIT_EXCEEDED"
"RATE_LIMIT_EXCEEDED",
"BOUND_IP_MISMATCH"
],
"example": "VALID",
"description": "The result of the license verification.\r\n- `VALID`: The license is valid\r\n- `NOT_FOUND`: The license was not found\r\n- `NOT_ACTIVE`: The license is not active\r\n- `EXPIRED`: The license has expired\r\n- `LICENSE_SCOPE_FAILED`: The scope of the license does not match the scope in the request\r\n- `IP_LIMIT_EXCEEDED`: The IP limit has been exceeded\r\n- `RATE_LIMIT_EXCEEDED`: The rate limit has been exceeded"
"description": "The result of the license verification.\r\n- `VALID`: The license is valid\r\n- `NOT_FOUND`: The license was not found\r\n- `NOT_ACTIVE`: The license is not active\r\n- `EXPIRED`: The license has expired\r\n- `LICENSE_SCOPE_FAILED`: The scope of the license does not match the scope in the request\r\n- `IP_LIMIT_EXCEEDED`: The IP limit has been exceeded\r\n- `RATE_LIMIT_EXCEEDED`: The rate limit has been exceeded\r\n- 'BOUND_IP_MISMATCH': The request was made from an IP address that does not match the one bound to the license"
},
"ValidationResponse": {
"properties": {
Expand Down