Skip to content

Commit

Permalink
Merge pull request #403 from sinamics/stats-api
Browse files Browse the repository at this point in the history
Added statistics api
  • Loading branch information
sinamics committed May 9, 2024
2 parents 167b030 + 68e7d02 commit 026fdef
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 18 deletions.
47 changes: 47 additions & 0 deletions docs/docs/Rest Api/Application/_source/stats.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
openapi: 3.1.0
info:
title: ZTNet Statistics Rest API
# version: 1.0.0
description: |
Access the ZTNet Statistics API to get various statistics that can be used in 3rd party applications.
Available from version v0.6.4.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
servers:
- url: https://ztnet.network/api/v1
description: ZTNet API
variables:
version:
default: v1
description: API version

components:
$ref: '../../_schema/security.yml#/components'

security:
- x-ztnet-auth: []

paths:
/stats:
get:
summary: Returns statistics for ztnet
description: |
Returns various statistics that can be used in 3rd party applications.
Available from version v0.6.4
operationId: getAppStats
responses:
200:
description: An object of statistics
content:
application/json:
schema:
$ref: '../../_schema/StatsSchema.yml#/StatsResponse'
example:
$ref: '../../_example/StatsExample.yml#/StatsExample'
401:
$ref: '../../_http_responses/Unauthorized.yml#/Unauthorized'
429:
$ref: '../../_http_responses/RateLimitExceeded.yml#/RateLimitExceeded'
500:
$ref: '../../_http_responses/InternalServerError.yml#/InternalServerError'
4 changes: 2 additions & 2 deletions docs/docs/Rest Api/Organization/_source/network.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
openapi: 3.1.0
info:
title: ZTNet Organization Network Rest API
version: 1.0.0
# version: 1.0.0
description: |
Access the ZTNet Organization suite through our RESTful Web API, compatible with version 0.6.0 and beyond.
Access the ZTNet Organization suite through our RESTful Web API.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/Rest Api/Organization/_source/networkMember.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
openapi: 3.1.0
info:
title: ZTNet Organization Member Rest API
version: 1.0.0
title: ZTNet Organization Network Member Rest API
# version: 1.0.0
description: |
Access the ZTNet Organization suite through our RESTful Web API, compatible with version 0.6.0 and beyond.
Access the ZTNet Organization suite through our RESTful Web API.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/Rest Api/Organization/_source/organization.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
openapi: 3.1.0
info:
title: ZTNet Network Rest API
version: 1.0.0
title: ZTNet Organization Rest API
# version: 1.0.0
description: |
Access the ZTNet Organization suite through our RESTful Web API, compatible with version 0.6.0 and beyond.
Access the ZTNet Organization suite through our RESTful Web API.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/Rest Api/Organization/_source/users.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
openapi: 3.1.0
info:
title: ZTNet Organization User Rest API
version: 1.0.0
# version: 1.0.0
description: |
The official ZTNet Organization Web API, beginning with version 0.6.0, provides public access with a rate limit of 50 requests per minute to maintain optimal service performance.
The official ZTNet Organization Web API, provides public access with a rate limit of 50 requests per minute to maintain optimal service performance.
servers:
- url: https://ztnet.network/api/v1
description: ZTNet API
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/Rest Api/Personal/_source/network.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
openapi: 3.1.0
info:
title: ZTNet Network Rest API
version: 1.0.0
# version: 1.0.0
description: |
Access the ZTNet suite through our RESTful Web API, compatible with version 0.4.0 and beyond.
Access the ZTNet suite through our RESTful Web API.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/Rest Api/Personal/_source/networkMember.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
openapi: 3.1.0
info:
title: ZTNet Network Rest API
version: 1.0.0
title: ZTNet Network Member Rest API
# version: 1.0.0
description: |
Access the ZTNet suite through our RESTful Web API, compatible with version 0.4.0 and beyond.
Access the ZTNet suite through our RESTful Web API.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
Expand Down
7 changes: 5 additions & 2 deletions docs/docs/Rest Api/Personal/_source/user.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
openapi: 3.1.0
info:
title: ZTNet User Rest API
version: 1.0.0
# version: 1.0.0
description: |
The official ZTNet Web API, beginning with version 0.4.0, provides public access with a rate limit of 50 requests per minute to maintain optimal service performance.
Access the ZTNet suite through our RESTful Web API.
This interface is subject to a rate limit of 50 requests per minute to ensure service reliability.
servers:
- url: https://ztnet.network/api/v1
description: ZTNet API
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/Rest Api/_example/StatsExample.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
StatsExample:
- users: 1
networks: 8
networkMembers: 7
appVersion: "v0.6.3"
loginsLast24h: 1
pendingUserInvitations: 0
activeWebhooks: 0
ztnetUptime: 735.491790795
registrationEnabled: false
hasPrivatRoot: true
34 changes: 34 additions & 0 deletions docs/docs/Rest Api/_schema/StatsSchema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
StatsResponse:
type: object
properties:
users:
type: integer
description: The number of users.
networks:
type: integer
description: The number of networks.
networkMembers:
type: integer
description: The total number of network members.
appVersion:
type: string
description: The current version of the ztnet application.
loginsLast24h:
type: integer
description: The number of logins in the last 24 hours.
pendingUserInvitations:
type: integer
description: The number of pending user invitations.
activeWebhooks:
type: integer
description: The number of active webhooks.
ztnetUptime:
type: number
format: float
description: The uptime of ztnet in minutes.
registrationEnabled:
type: boolean
description: Indicates if registration is enabled.
hasPrivatRoot:
type: boolean
description: Indicates if a private root is currently in use.
7 changes: 7 additions & 0 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const config = {
id: "api", // plugin id
docsPluginId: "classic", // id of plugin-content-docs or preset for rendering docs
config: {
stats:{
specPath: "docs/Rest Api/Application/_source/stats.yml", // path to OpenAPI spec, URLs supported
outputDir: "docs/Rest Api/Application/Statistics", // output directory for generated files
sidebarOptions: { // optional, instructs plugin to generate sidebar.js
groupPathsBy: "tag", // group sidebar items by operation "tag"
},
},
// Personal Controller
personal_user: {
specPath: "docs/Rest Api/Personal/_source/user.yml", // path to OpenAPI spec, URLs supported
Expand Down
44 changes: 44 additions & 0 deletions src/pages/api/__tests__/v1/application/statistic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import apiStatsHandler from "~/pages/api/v1/stats/index";
import { NextApiRequest, NextApiResponse } from "next";

describe("/api/stats", () => {
it("should allow only GET method", async () => {
const methods = ["DELETE", "POST", "PUT", "PATCH", "OPTIONS", "HEAD"];
const req = {} as NextApiRequest;
const res = {
status: jest.fn().mockReturnThis(),
end: jest.fn(),
json: jest.fn().mockReturnThis(),
setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it
} as unknown as NextApiResponse;

for (const method of methods) {
req.method = method;
await apiStatsHandler(req, res);

expect(res.status).toHaveBeenCalledWith(405);

// expect json to be called with text "Method Not Allowed"
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({ error: "Method Not Allowed" }),
);
}
});

it("should respond 401 when invalid API key for GET", async () => {
const req = {
method: "GET",
headers: { "x-ztnet-auth": "invalidApiKey" },
} as unknown as NextApiRequest;
const res = {
status: jest.fn().mockReturnThis(),
end: jest.fn(),
json: jest.fn().mockReturnThis(),
setHeader: jest.fn(), // Mock `setHeader` rate limiter uses it
} as unknown as NextApiResponse;

await apiStatsHandler(req, res);

expect(res.status).toHaveBeenCalledWith(401);
});
});
106 changes: 106 additions & 0 deletions src/pages/api/v1/stats/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "~/server/db";
import { AuthorizationType } from "~/types/apiTypes";
import { decryptAndVerifyToken } from "~/utils/encryption";
import { handleApiErrors } from "~/utils/errors";
import { globalSiteVersion } from "~/utils/global";
import rateLimit from "~/utils/rateLimit";

// Number of allowed requests per minute
const limiter = rateLimit({
interval: 60 * 1000, // 60 seconds
uniqueTokenPerInterval: 500, // Max 500 users per second
});

const REQUEST_PR_MINUTE = 50;

export default async function createStatsHandler(
req: NextApiRequest,
res: NextApiResponse,
) {
try {
await limiter.check(res, REQUEST_PR_MINUTE, "CREATE_USER_CACHE_TOKEN"); // 10 requests per minute
} catch {
return res.status(429).json({ error: "Rate limit exceeded" });
}

// create a switch based on the HTTP method
switch (req.method) {
case "GET":
await GET_stats(req, res);
break;
default: // Method Not Allowed
res.status(405).json({ error: "Method Not Allowed" });
break;
}
}

export const GET_stats = async (req: NextApiRequest, res: NextApiResponse) => {
const apiKey = req.headers["x-ztnet-auth"] as string;

const requireAdmin = true;

try {
await decryptAndVerifyToken({
apiKey,
apiAuthorizationType: AuthorizationType.PERSONAL,
requireAdmin,
});

// get number of users
const users = await prisma.user.count();

// get number of networks
const networks = await prisma.network.count();

// get number of members
const networkMembers = await prisma.network_members.count();

// get application version
const appVersion = globalSiteVersion;

// get logins last 24 hours
const loginsLast24h = await prisma.user.count({
where: {
lastLogin: {
gte: new Date(new Date().getTime() - 24 * 60 * 60 * 1000),
},
},
});

// get UserInvitation
const pendingUserInvitations = await prisma.userInvitation.count();

// get pending Webhook
const activeWebhooks = await prisma.webhook.count();

// get uptime
const ztnetUptime = process.uptime();

// get if customPlanetUsed is used
const rootServer = await prisma.planet.count();

// get global options
const globalOptions = await prisma.globalOptions.findFirst({
where: {
id: 1,
},
});

// return all json
return res.status(200).json({
users,
networks,
networkMembers,
appVersion,
loginsLast24h,
pendingUserInvitations,
activeWebhooks,
ztnetUptime,
registrationEnabled: globalOptions?.enableRegistration || false,
hasPrivatRoot: !!rootServer,
});
} catch (cause) {
return handleApiErrors(cause, res);
}
};
18 changes: 18 additions & 0 deletions src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ export const authOptions: NextAuthOptions = {
*/
async signIn({ user, account }) {
if (account.provider === "credentials") {
// Check if the user already exists
const existingUser = await prisma.user.findUnique({
where: {
email: user.email,
},
});

if (existingUser) {
// User exists, update last login or other fields as necessary
await prisma.user.update({
where: {
id: existingUser.id,
},
data: {
lastLogin: new Date().toISOString(),
},
});
}
return true;
}
if (account.provider === "oauth") {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ import { env } from "~/env.mjs";
// If it is not set, use the default value "ZTnet".
export const globalSiteTitle = process.env.NEXT_PUBLIC_SITE_NAME || "ZTNET";

export const globalSiteVersion = env.NEXT_PUBLIC_APP_VERSION || null;
export const globalSiteVersion = env.NEXT_PUBLIC_APP_VERSION || "development";

0 comments on commit 026fdef

Please sign in to comment.