diff --git a/.vscode/settings.json b/.vscode/settings.json index 146a841d..963db5c7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,8 @@ }, "yaml.schemas": { "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.1/schema.json": "file:///workspaces/docs/ztnet/docs/Api/user/api.yml" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 979b6b3d..73feec8a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,7 +69,8 @@ ENV NEXTAUTH_URL_INTERNAL http://localhost:3000 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs RUN apt update && apt install -y curl sudo postgresql-client && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -RUN npm install @prisma/client +# need to install these package for seeding the database +RUN npm install @prisma/client @paralleldrive/cuid2 RUN npm install -g prisma ts-node RUN mkdir -p /var/lib/zerotier-one && chown -R nextjs:nodejs /var/lib/zerotier-one && chmod -R 777 /var/lib/zerotier-one diff --git a/docs/ztnet/docs/Licensing Notice/mkworld.md b/docs/ztnet/docs/Licensing Notice/mkworld.md index 2129b425..efc984f1 100644 --- a/docs/ztnet/docs/Licensing Notice/mkworld.md +++ b/docs/ztnet/docs/Licensing Notice/mkworld.md @@ -5,8 +5,11 @@ slug: /licensing/mkworld description: Attribution and Licensing Notice for Third-Party Components sidebar_position: 5 --- + # Mkworld Tool + ### 📄 Attribution and Licensing Notice for Third-Party Components + ZTNET utilizes the mkworld tool, written in Go, to generate the custom planet file. While the original mkworld tool was developed by ZeroTier, the version we are using was adapted and re-implemented in Go by Patrick Young (@kmahyyg). This Go adaptation is licensed under the GNU General Public License v3.0. We would like to express our appreciation to Patrick Young (@kmahyyg) for his efforts in creating this Go version, which has benefited our project. -Our project, in its entirety, is also licensed under the GNU General Public License v3.0. For a comprehensive understanding of our project's licensing terms, please consult our LICENSE file. \ No newline at end of file +Our project, in its entirety, is also licensed under the GNU General Public License v3.0. For a comprehensive understanding of our project's licensing terms, please consult our LICENSE file. diff --git a/docs/ztnet/docs/Rest Api/Network/create-new-network.api.mdx b/docs/ztnet/docs/Rest Api/Network/create-new-network.api.mdx new file mode 100644 index 00000000..64b86c44 --- /dev/null +++ b/docs/ztnet/docs/Rest Api/Network/create-new-network.api.mdx @@ -0,0 +1,46 @@ +--- +id: create-new-network +title: "Create New Network" +description: "Create New Network" +sidebar_label: "Create New Network" +hide_title: true +hide_table_of_contents: true +api: {"operationId":"createNewNetwork","parameters":[{"name":"x-ztnet-auth","in":"header","required":true,"schema":{"type":"string"},"description":"API Key for the user"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":["string"],"required":false,"description":"Name of the network. If not provided, a random name will be generated."}}}}}},"responses":{"200":{"description":"New Network Created","content":{"application/json":{"schema":{"type":"object","properties":{"authTokens":{"type":"array","items":{"type":"string","nullable":true}},"authorizationEndpoint":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}},"clientId":{"type":"string"},"creationTime":{"type":"integer"},"dns":{"type":"array","items":{"type":"string"}},"enableBroadcast":{"type":"boolean"},"id":{"type":"string"},"ipAssignmentPools":{"type":"array","items":{"type":"object","properties":{"ipRangeEnd":{"type":"string"},"ipRangeStart":{"type":"string"}}}},"mtu":{"type":"integer"},"multicastLimit":{"type":"integer"},"name":{"type":"string"},"nwid":{"type":"string"},"objtype":{"type":"string"},"private":{"type":"boolean"},"remoteTraceLevel":{"type":"integer"},"remoteTraceTarget":{"type":"string"},"revision":{"type":"integer"},"routes":{"type":"array","items":{"type":"object","properties":{"target":{"type":"string"},"via":{"type":"string","nullable":true}}}},"rules":{"type":"array","items":{"type":"object","properties":{"not":{"type":"boolean"},"or":{"type":"boolean"},"type":{"type":"string"}}}},"rulesSource":{"type":"string"},"ssoEnabled":{"type":"boolean"},"tags":{"type":"array","items":{"type":"string"}},"v4AssignMode":{"type":"object","properties":{"zt":{"type":"boolean"}}},"v6AssignMode":{"type":"object","properties":{"6plane":{"type":"boolean"},"rfc4193":{"type":"boolean"},"zt":{"type":"boolean"}}}},"title":"NetworkResponse"},"example":{"NetworkExample":{"authTokens":[null],"authorizationEndpoint":"","capabilities":[],"clientId":"","creationTime":1698676570111,"dns":[],"enableBroadcast":true,"id":"network_id","ipAssignmentPools":[{"ipRangeEnd":"172.25.25.254","ipRangeStart":"172.25.25.1"}],"mtu":2800,"multicastLimit":32,"name":"slimy-earwig","nwid":"network_id","objtype":"network","private":true,"remoteTraceLevel":0,"remoteTraceTarget":null,"revision":1,"routes":[{"target":"172.25.25.0/24","via":null}],"rules":[{"not":false,"or":false,"type":"ACTION_ACCEPT"}],"rulesSource":"","ssoEnabled":false,"tags":[],"v4AssignMode":{"zt":true},"v6AssignMode":{"6plane":false,"rfc4193":false,"zt":false}}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}},"description":"Create New Network","method":"post","path":"/network","jsonRequestBodyExample":{"name":"string"},"info":{"title":"ZTNet Web API","info_path":"restapi/ztnet-web-api","version":"1.0.0","description":"Public API for ZTNet. Available from ztnet version 0.4.0 onwards.\n\nThis API is rate-limited to 10 requests per minute\n"},"postman":{"name":"Create New Network","description":{"type":"text/plain"},"url":{"path":["network"],"host":["{{baseUrl}}"],"query":[],"variable":[]},"header":[{"disabled":false,"description":{"content":"(Required) API Key for the user","type":"text/plain"},"key":"x-ztnet-auth","value":""},{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"method":"POST","body":{"mode":"raw","raw":"\"\"","options":{"raw":{"language":"json"}}}}} +sidebar_class_name: "post api-method" +info_path: Rest Api/Network/ztnet-web-api +custom_edit_url: null +--- + +import ApiTabs from "@theme/ApiTabs"; +import MimeTabs from "@theme/MimeTabs"; +import ParamsItem from "@theme/ParamsItem"; +import ResponseSamples from "@theme/ResponseSamples"; +import SchemaItem from "@theme/SchemaItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import TabItem from "@theme/TabItem"; + +## Create New Network + + + +Create New Network + +
Header Parameters
Request Body
+ +New Network Created + +
Schema
    ipAssignmentPools object[]
  • Array [
  • ]
  • routes object[]
  • Array [
  • ]
  • rules object[]
  • Array [
  • ]
  • v4AssignMode object
    v6AssignMode object
+ +Unauthorized + +
Schema
+ +Rate limit exceeded + +
Schema
+ +Internal server error + +
Schema
+ \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/Network/get-user-networks.api.mdx b/docs/ztnet/docs/Rest Api/Network/get-user-networks.api.mdx new file mode 100644 index 00000000..4dcdef51 --- /dev/null +++ b/docs/ztnet/docs/Rest Api/Network/get-user-networks.api.mdx @@ -0,0 +1,50 @@ +--- +id: get-user-networks +title: "Returns a list of Networks you have access to" +description: "Returns a list of Networks you have access to" +sidebar_label: "Returns a list of Networks you have access to" +hide_title: true +hide_table_of_contents: true +api: {"operationId":"getUserNetworks","parameters":[{"name":"x-ztnet-auth","in":"header","required":true,"schema":{"type":"string"},"description":"API Key for the user"}],"responses":{"200":{"description":"An array of Network IDs","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}},"examples":{"example1":{"value":["networkid#1","networkid#2"]}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}},"description":"Returns a list of Networks you have access to","method":"get","path":"/network","info":{"title":"ZTNet Web API","info_path":"restapi/ztnet-web-api","version":"1.0.0","description":"Public API for ZTNet. Available from ztnet version 0.4.0 onwards.\n\nThis API is rate-limited to 10 requests per minute\n"},"postman":{"name":"Returns a list of Networks you have access to","description":{"type":"text/plain"},"url":{"path":["network"],"host":["{{baseUrl}}"],"query":[],"variable":[]},"header":[{"disabled":false,"description":{"content":"(Required) API Key for the user","type":"text/plain"},"key":"x-ztnet-auth","value":""},{"key":"Accept","value":"application/json"}],"method":"GET"}} +sidebar_class_name: "get api-method" +info_path: Rest Api/Network/ztnet-web-api +custom_edit_url: null +--- + +import ApiTabs from "@theme/ApiTabs"; +import MimeTabs from "@theme/MimeTabs"; +import ParamsItem from "@theme/ParamsItem"; +import ResponseSamples from "@theme/ResponseSamples"; +import SchemaItem from "@theme/SchemaItem"; +import SchemaTabs from "@theme/SchemaTabs"; +import DiscriminatorTabs from "@theme/DiscriminatorTabs"; +import TabItem from "@theme/TabItem"; + +## Returns a list of Networks you have access to + + + +Returns a list of Networks you have access to + +
Header Parameters
+ +An array of Network IDs + +
Schema
  • Array [
  • + +string + +
  • ]
+ +Unauthorized + +
Schema
+ +Rate limit exceeded + +
Schema
+ +Internal server error + +
Schema
+ \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/Network/sidebar.js b/docs/ztnet/docs/Rest Api/Network/sidebar.js new file mode 100644 index 00000000..be2d2d80 --- /dev/null +++ b/docs/ztnet/docs/Rest Api/Network/sidebar.js @@ -0,0 +1 @@ +module.exports = [{"type":"doc","id":"Rest Api/Network/ztnet-web-api"},{"type":"category","label":"UNTAGGED","items":[{"type":"doc","id":"Rest Api/Network/get-user-networks","label":"Returns a list of Networks you have access to","className":"api-method get"},{"type":"doc","id":"Rest Api/Network/create-new-network","label":"Create New Network","className":"api-method post"}]}]; \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/ztnet-web-api.info.mdx b/docs/ztnet/docs/Rest Api/Network/ztnet-web-api.info.mdx similarity index 56% rename from docs/ztnet/docs/Rest Api/ztnet-web-api.info.mdx rename to docs/ztnet/docs/Rest Api/Network/ztnet-web-api.info.mdx index 7994e028..be2a5478 100644 --- a/docs/ztnet/docs/Rest Api/ztnet-web-api.info.mdx +++ b/docs/ztnet/docs/Rest Api/Network/ztnet-web-api.info.mdx @@ -24,9 +24,5 @@ Public API for ZTNet. Available from ztnet version 0.4.0 onwards. This API is rate-limited to 10 requests per minute -

Authentication

-API key can be generated from the ZTNet admin section. - -
Security Scheme Type:apiKey
Header parameter name:x-ztnet-auth
\ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/create-a-new-user.api.mdx b/docs/ztnet/docs/Rest Api/User/post-new-user.api.mdx similarity index 67% rename from docs/ztnet/docs/Rest Api/create-a-new-user.api.mdx rename to docs/ztnet/docs/Rest Api/User/post-new-user.api.mdx index 4d992336..828c6f0b 100644 --- a/docs/ztnet/docs/Rest Api/create-a-new-user.api.mdx +++ b/docs/ztnet/docs/Rest Api/User/post-new-user.api.mdx @@ -1,13 +1,13 @@ --- -id: create-a-new-user +id: post-new-user title: "Create a new user" description: "If no users have been created yet, no API key is required. Otherwise, an API key must be included in the request header." sidebar_label: "Create a new user" hide_title: true hide_table_of_contents: true -api: {"description":"If no users have been created yet, no API key is required. Otherwise, an API key must be included in the request header.\nKeep in mind that first user created will be the admin user.\n","security":[{"x-ztnet-auth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["email","password","name"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"name":{"type":"string"},"expiresAt":{"type":"string | null","description":"The date and time at which the user's account will expire. If null, the account will never expire.\nMust be in ISO 8601 format (e.g. 2023-10-28T00:00:00Z).\n\nNot applicable if the user is an admin (first user). Admin accounts never expire.\n"}}},"example":{"email":"test@example.com","password":"password123","name":"Test User","expiresAt":"2023-10-28T00:00:00Z"}}}},"responses":{"200":{"description":"User successfully created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"expiresAt":{"type":"string"}},"example":{"id":"12345","email":"test@example.com","name":"Test User","expiresAt":"2023-10-28T00:00:00Z"}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}},"method":"post","path":"/api/v1/user","securitySchemes":{"x-ztnet-auth":{"type":"apiKey","required":true,"in":"header","name":"x-ztnet-auth","description":"API key can be generated from the ZTNet admin section."}},"jsonRequestBodyExample":{"email":"string","password":"string","name":"string"},"info":{"title":"ZTNet Web API","info_path":"restapi/ztnet-web-api","version":"1.0.0","description":"Public API for ZTNet. Available from ztnet version 0.4.0 onwards.\n\nThis API is rate-limited to 10 requests per minute\n"},"postman":{"name":"Create a new user","description":{"content":"If no users have been created yet, no API key is required. Otherwise, an API key must be included in the request header.\nKeep in mind that first user created will be the admin user.\n","type":"text/plain"},"url":{"path":["api","v1","user"],"host":["{{baseUrl}}"],"query":[],"variable":[]},"header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"method":"POST","body":{"mode":"raw","raw":"\"\"","options":{"raw":{"language":"json"}}},"auth":{"type":"apikey","apikey":[{"type":"any","value":"x-ztnet-auth","key":"key"},{"type":"any","value":"","key":"value"},{"type":"any","value":"header","key":"in"}]}}} +api: {"tags":["user"],"operationId":"postNewUser","description":"If no users have been created yet, no API key is required. Otherwise, an API key must be included in the request header.\nKeep in mind that first user created will be the admin user.\n","security":[{"x-ztnet-auth":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["email","password","name"],"properties":{"email":{"type":"string"},"password":{"type":"string"},"name":{"type":"string"},"expiresAt":{"type":"string | null","description":"The date and time at which the user's account will expire. If null, the account will never expire.\nMust be in ISO 8601 format (e.g. 2023-10-28T00:00:00Z).\n\nNot applicable if the user is an admin (first user). Admin accounts never expire.\n"}}},"example":{"email":"post@ztnet.network","password":"strong_password","name":"Ztnet User","expiresAt":"2023-10-28T00:00:00Z"}}}},"responses":{"200":{"description":"User successfully created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"expiresAt":{"type":"string"}},"example":{"id":"12345","email":"test@example.com","name":"Test User","expiresAt":"2023-10-28T00:00:00Z"}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}},"method":"post","path":"/user","servers":[{"url":"https://ztnet.network/api/v1","description":"ZTNet API","variables":{"version":{"default":"v1","description":"API version"}}}],"jsonRequestBodyExample":{"email":"string","password":"string","name":"string"},"info":{"title":"ZTNet Web API","info_path":"restapi/ztnet-web-api","version":"1.0.0","description":"Public API for ZTNet. Available from ztnet version 0.4.0 onwards.\n\nThis API is rate-limited to 10 requests per minute\n"},"postman":{"name":"Create a new user","description":{"content":"If no users have been created yet, no API key is required. Otherwise, an API key must be included in the request header.\nKeep in mind that first user created will be the admin user.\n","type":"text/plain"},"url":{"path":["user"],"host":["{{baseUrl}}"],"query":[],"variable":[]},"header":[{"key":"Content-Type","value":"application/json"},{"key":"Accept","value":"application/json"}],"method":"POST","body":{"mode":"raw","raw":"\"\"","options":{"raw":{"language":"json"}}}}} sidebar_class_name: "post api-method" -info_path: Rest Api/ztnet-web-api +info_path: Rest Api/User/ztnet-web-api custom_edit_url: null --- diff --git a/docs/ztnet/docs/Rest Api/User/sidebar.js b/docs/ztnet/docs/Rest Api/User/sidebar.js new file mode 100644 index 00000000..b9f7ff8a --- /dev/null +++ b/docs/ztnet/docs/Rest Api/User/sidebar.js @@ -0,0 +1 @@ +module.exports = [{"type":"doc","id":"Rest Api/User/ztnet-web-api"},{"type":"category","label":"Users dsd s","link":{"type":"generated-index","title":"user","slug":"/category/Rest Api/User/user"},"items":[{"type":"doc","id":"Rest Api/User/post-new-user","label":"Create a new user","className":"api-method post"}]},{"type":"category","label":"UNTAGGED","items":[{"type":"doc","id":"Rest Api/User/post-new-user","label":"postNewUser","className":"api-method components"}]}]; \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/User/ztnet-web-api.info.mdx b/docs/ztnet/docs/Rest Api/User/ztnet-web-api.info.mdx new file mode 100644 index 00000000..be2a5478 --- /dev/null +++ b/docs/ztnet/docs/Rest Api/User/ztnet-web-api.info.mdx @@ -0,0 +1,28 @@ +--- +id: ztnet-web-api +title: "ZTNet Web API" +description: "Public API for ZTNet. Available from ztnet version 0.4.0 onwards." +sidebar_label: Introduction +sidebar_position: 0 +hide_title: true +custom_edit_url: null +--- + +import ApiLogo from "@theme/ApiLogo"; +import SchemaTabs from "@theme/SchemaTabs"; +import TabItem from "@theme/TabItem"; +import Export from "@theme/ApiDemoPanel/Export"; + +Version: 1.0.0 + +# ZTNet Web API + + + +Public API for ZTNet. Available from ztnet version 0.4.0 onwards. + +This API is rate-limited to 10 requests per minute + + + + \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/_example/NetworkExample.yml b/docs/ztnet/docs/Rest Api/_example/NetworkExample.yml new file mode 100644 index 00000000..f85707e9 --- /dev/null +++ b/docs/ztnet/docs/Rest Api/_example/NetworkExample.yml @@ -0,0 +1,37 @@ +NetworkExample: + authTokens: [null] + authorizationEndpoint: "" + capabilities: [] + clientId: "" + creationTime: 1698676570111 + dns: [] + enableBroadcast: true + id: "network_id" + ipAssignmentPools: + - ipRangeEnd: "172.25.25.254" + ipRangeStart: "172.25.25.1" + mtu: 2800 + multicastLimit: 32 + name: "slimy-earwig" + nwid: "network_id" + objtype: "network" + private: true + remoteTraceLevel: 0 + remoteTraceTarget: null + revision: 1 + routes: + - target: "172.25.25.0/24" + via: null + rules: + - not: false + or: false + type: "ACTION_ACCEPT" + rulesSource: "" + ssoEnabled: false + tags: [] + v4AssignMode: + zt: true + v6AssignMode: + 6plane: false + rfc4193: false + zt: false \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/_schema/NetworkResponse.yml b/docs/ztnet/docs/Rest Api/_schema/NetworkResponse.yml new file mode 100644 index 00000000..ae0a362d --- /dev/null +++ b/docs/ztnet/docs/Rest Api/_schema/NetworkResponse.yml @@ -0,0 +1,96 @@ +NetworkResponse: + type: object + properties: + authTokens: + type: array + items: + type: string + nullable: true + authorizationEndpoint: + type: string + capabilities: + type: array + items: + type: string + clientId: + type: string + creationTime: + type: integer + dns: + type: array + items: + type: string + enableBroadcast: + type: boolean + id: + type: string + ipAssignmentPools: + type: array + items: + type: object + properties: + ipRangeEnd: + type: string + ipRangeStart: + type: string + mtu: + type: integer + multicastLimit: + type: integer + name: + type: string + nwid: + type: string + objtype: + type: string + private: + type: boolean + remoteTraceLevel: + type: integer + remoteTraceTarget: + type: string + revision: + type: integer + routes: + type: array + items: + type: object + properties: + target: + type: string + via: + type: string + nullable: true + rules: + type: array + items: + type: object + properties: + not: + type: boolean + or: + type: boolean + type: + type: string + rulesSource: + type: string + ssoEnabled: + type: boolean + tags: + type: array + items: + type: string + v4AssignMode: + type: object + properties: + zt: + type: boolean + v6AssignMode: + type: object + properties: + 6plane: + type: boolean + rfc4193: + type: boolean + zt: + type: boolean \ No newline at end of file diff --git a/docs/ztnet/docs/Rest Api/_source/network.yml b/docs/ztnet/docs/Rest Api/_source/network.yml new file mode 100644 index 00000000..123b0b54 --- /dev/null +++ b/docs/ztnet/docs/Rest Api/_source/network.yml @@ -0,0 +1,122 @@ +openapi: 3.1.0 +info: + title: ZTNet Web API + info_path: restapi/ztnet-web-api + version: 1.0.0 + description: | + Public API for ZTNet. Available from ztnet version 0.4.0 onwards. + + This API is rate-limited to 50 requests per minute +paths: + /network: + get: + summary: Returns a list of Networks you have access to + operationId: getUserNetworks + parameters: + - name: x-ztnet-auth + in: header + required: true + schema: + type: string + description: API Key for the user + responses: + 200: + description: An array of Network IDs + content: + application/json: + schema: + type: array + items: + type: string + examples: + example1: + value: ["networkid#1", "networkid#2"] + 401: + description: Unauthorized + content: + application/json: + schema: + type: object + properties: + error: + type: string + 429: + description: Rate limit exceeded + content: + application/json: + schema: + type: object + properties: + error: + type: string + 500: + description: Internal server error + content: + application/json: + schema: + type: object + properties: + message: + type: string + # POST /network + post: + summary: Create New Network + operationId: createNewNetwork + parameters: + - name: x-ztnet-auth + in: header + required: true + schema: + type: string + description: API Key for the user + requestBody: + required: false + content: + application/json: + schema: + type: object + # required: + # - name + properties: + name: + type: + - string + required: false + description: Name of the network. If not provided, a random name will be generated. + responses: + 200: + description: New Network Created + content: + application/json: + schema: + $ref: '../_schema/NetworkResponse.yml#/NetworkResponse' + example: + $ref: '../_example/NetworkExample.yml' + + 401: + description: Unauthorized + content: + application/json: + schema: + type: object + properties: + error: + type: string + 429: + description: Rate limit exceeded + content: + application/json: + schema: + type: object + properties: + error: + type: string + 500: + description: Internal server error + content: + application/json: + schema: + type: object + properties: + message: + type: string diff --git a/docs/ztnet/docs/Rest Api/source/user.yml b/docs/ztnet/docs/Rest Api/_source/user.yml similarity index 80% rename from docs/ztnet/docs/Rest Api/source/user.yml rename to docs/ztnet/docs/Rest Api/_source/user.yml index 39e34a33..da90c58e 100644 --- a/docs/ztnet/docs/Rest Api/source/user.yml +++ b/docs/ztnet/docs/Rest Api/_source/user.yml @@ -6,11 +6,25 @@ info: description: | Public API for ZTNet. Available from ztnet version 0.4.0 onwards. - This API is rate-limited to 10 requests per minute + This API is rate-limited to 50 requests per minute +servers: + - url: https://ztnet.network/api/v1 + description: ZTNet API + variables: + version: + default: v1 + description: API version +tags: + - name: user + description: Users API + x-displayName: Users dsd s paths: - /api/v1/user: + /user: post: + tags: + - user summary: Create a new user + operationId: postNewUser description: | If no users have been created yet, no API key is required. Otherwise, an API key must be included in the request header. Keep in mind that first user created will be the admin user. @@ -41,9 +55,9 @@ paths: Not applicable if the user is an admin (first user). Admin accounts never expire. example: - email: "test@example.com" - password: "password123" - name: "Test User" + email: "post@ztnet.network" + password: "strong_password" + name: "Ztnet User" expiresAt: "2023-10-28T00:00:00Z" # parameters: # - name: x-ztnet-auth @@ -102,11 +116,12 @@ paths: properties: message: type: string -components: - securitySchemes: - x-ztnet-auth: - type: apiKey - required: true - in: header - name: x-ztnet-auth - description: API key can be generated from the ZTNet admin section. + components: + operationId: postNewUser + securitySchemes: + x-ztnet-auth: + type: apiKey + required: true + in: header + name: x-ztnet-auth + description: API key can be generated from the ZTNet admin section. diff --git a/docs/ztnet/docusaurus.config.js b/docs/ztnet/docusaurus.config.js index 11ca821c..719d0a61 100644 --- a/docs/ztnet/docusaurus.config.js +++ b/docs/ztnet/docusaurus.config.js @@ -39,10 +39,27 @@ const config = { id: "api", // plugin id docsPluginId: "classic", // id of plugin-content-docs or preset for rendering docs config: { + // info: { // the referenced when running CLI commands + // specPath: "docs/Rest Api/info.yml", // path to OpenAPI spec, URLs supported + // outputDir: "docs/Rest Api", // output directory for generated files + // sidebarOptions: { // optional, instructs plugin to generate sidebar.js + // groupPathsBy: "tag", // group sidebar items by operation "tag" + // }, + // }, user: { // the referenced when running CLI commands - specPath: "docs/Rest Api/source/user.yml", // path to OpenAPI spec, URLs supported - outputDir: "docs/Rest Api" // output directory for generated files + specPath: "docs/Rest Api/_source/user.yml", // path to OpenAPI spec, URLs supported + outputDir: "docs/Rest Api/User", // output directory for generated files + sidebarOptions: { // optional, instructs plugin to generate sidebar.js + groupPathsBy: "tag", // group sidebar items by operation "tag" + }, }, + network: { // the for network + specPath: "docs/Rest Api/_source/network.yml", // path to OpenAPI spec, URLs supported + outputDir: "docs/Rest Api/Network", // output directory for network files + sidebarOptions: { // optional, instructs plugin to generate sidebar.js + groupPathsBy: "tag", // group sidebar items by operation "tag" + }, + } } }, ] diff --git a/package-lock.json b/package-lock.json index e0f91fd1..66750d21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@faker-js/faker": "^8.0.2", "@heroicons/react": "^2.0.16", "@next-auth/prisma-adapter": "^1.0.5", + "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.1.1", "@tanstack/react-query": "^4.20.2", "@tanstack/react-query-devtools": "^4.28.0", @@ -34,6 +35,7 @@ "formidable": "^3.5.0", "ip-address": "^8.1.0", "jsonwebtoken": "^9.0.1", + "lru-cache": "^10.0.1", "next": "13.4.10", "next-auth": "4.22.1", "next-intl": "2.19.0", @@ -1682,9 +1684,9 @@ } }, "node_modules/@lezer/javascript": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.8.tgz", - "integrity": "sha512-QRmw/5xrcyRLyWr3JT3KCzn2XZr5NYNqQMGsqnYy+FghbQn9DZPuj6JDkE6uSXvfMLpdapu8KBIaeoJFaR4QVw==", + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.9.tgz", + "integrity": "sha512-7Uv8mBBE6l44spgWEZvEMdDqGV+FIuY7kJ1o5TFm+jxIuxydO3PcKJYiINij09igd1D/9P7l2KDqpkN8c3bM6A==", "dependencies": { "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" @@ -1910,6 +1912,17 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1953,6 +1966,14 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -3413,9 +3434,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001557", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001557.tgz", - "integrity": "sha512-91oR7hLNUP3gG6MLU+n96em322a8Xzes8wWdBKhLgUoiJsAF5irZnxSUCbc+qUZXNnPCfUwLOi9ZCZpkvjQajw==", + "version": "1.0.30001558", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001558.tgz", + "integrity": "sha512-/Et7DwLqpjS47JPEcz6VnxU9PwcIdVi0ciLXRWBQdj1XFye68pSQYpV0QtPTfUKWuOaEig+/Vez2l74eDc1tPQ==", "funding": [ { "type": "opencollective", @@ -4198,9 +4219,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.569", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz", - "integrity": "sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg==", + "version": "1.4.570", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.570.tgz", + "integrity": "sha512-5GxH0PLSIfXKOUMMHMCT4M0olwj1WwAxsQHzVW5Vh3kbsvGw8b4k7LHQmTLC2aRhsgFzrF57XJomca4XLc/WHA==", "dev": true }, "node_modules/emittery": { @@ -6595,9 +6616,9 @@ } }, "node_modules/jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -6947,7 +6968,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "dev": true, "engines": { "node": "14 || >=16.14" } @@ -8147,9 +8167,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -8908,9 +8928,9 @@ } }, "node_modules/streamx": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", - "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.2.tgz", + "integrity": "sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==", "dependencies": { "fast-fifo": "^1.1.0", "queue-tick": "^1.0.1" diff --git a/package.json b/package.json index 4ab4d235..801a92da 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@faker-js/faker": "^8.0.2", "@heroicons/react": "^2.0.16", "@next-auth/prisma-adapter": "^1.0.5", + "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.1.1", "@tanstack/react-query": "^4.20.2", "@tanstack/react-query-devtools": "^4.28.0", @@ -41,6 +42,7 @@ "formidable": "^3.5.0", "ip-address": "^8.1.0", "jsonwebtoken": "^9.0.1", + "lru-cache": "^10.0.1", "next": "13.4.10", "next-auth": "4.22.1", "next-intl": "2.19.0", @@ -100,4 +102,4 @@ "prisma": { "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" } -} \ No newline at end of file +} diff --git a/prisma/migrations/20231030072137_userid/migration.sql b/prisma/migrations/20231030072137_userid/migration.sql new file mode 100644 index 00000000..af3009b9 --- /dev/null +++ b/prisma/migrations/20231030072137_userid/migration.sql @@ -0,0 +1,60 @@ +/* + Warnings: + + - The primary key for the `User` table will be changed. If it partially fails, the table could be left without primary key constraint. + +*/ +-- DropForeignKey +ALTER TABLE "APIToken" DROP CONSTRAINT "APIToken_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Account" DROP CONSTRAINT "Account_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "Session" DROP CONSTRAINT "Session_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "UserOptions" DROP CONSTRAINT "UserOptions_userId_fkey"; + +-- DropForeignKey +ALTER TABLE "network" DROP CONSTRAINT "network_authorId_fkey"; + +-- AlterTable +ALTER TABLE "APIToken" ALTER COLUMN "userId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "Account" ALTER COLUMN "userId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "Session" ALTER COLUMN "userId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "User" DROP CONSTRAINT "User_pkey", +ALTER COLUMN "id" DROP DEFAULT, +ALTER COLUMN "id" SET DATA TYPE TEXT, +ADD CONSTRAINT "User_pkey" PRIMARY KEY ("id"); +DROP SEQUENCE "User_id_seq"; + +-- AlterTable +ALTER TABLE "UserInvitation" ALTER COLUMN "createdBy" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "UserOptions" ALTER COLUMN "userId" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "network" ALTER COLUMN "authorId" SET DATA TYPE TEXT; + +-- AddForeignKey +ALTER TABLE "network" ADD CONSTRAINT "network_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserOptions" ADD CONSTRAINT "UserOptions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "APIToken" ADD CONSTRAINT "APIToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 938218c6..489f8ded 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -82,10 +82,9 @@ model network { flowRule String? autoAssignIp Boolean? @default(true) nw_userid User @relation(fields: [authorId], references: [id], onDelete: Cascade) - authorId Int + authorId String tagsByName Json? capabilitiesByName Json? - networkMembers network_members[] notations Notation[] } @@ -119,7 +118,7 @@ model NetworkMemberNotation { // Necessary for Next auth model Account { id String @id @default(cuid()) - userId Int + userId String type String refresh_token String? // @db.Text access_token String? // @db.Text @@ -134,14 +133,14 @@ model Account { model Session { id String @id @default(cuid()) sessionToken String @unique - userId Int + userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) } model UserOptions { id Int @id @default(autoincrement()) - userId Int @unique + userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) //networks @@ -180,7 +179,7 @@ model APIToken { id Int @id @default(autoincrement()) name String token String @unique - userId Int + userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) expiresAt DateTime? // null means it never expires @@ -188,7 +187,7 @@ model APIToken { } model User { - id Int @id @default(autoincrement()) + id String @id @default(cuid()) name String email String @unique emailVerified DateTime? @@ -236,7 +235,7 @@ model UserInvitation { expires DateTime timesCanUse Int @default(1) timesUsed Int @default(0) - createdBy Int + createdBy String createdAt DateTime @default(now()) } diff --git a/prisma/seed.ts b/prisma/seed.ts index 4af35463..62ee186c 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,3 +1,4 @@ +import { updateUserId } from "./seeds/update-user-id"; import { seedUserOptions } from "./seeds/user-option.seed"; import { PrismaClient } from "@prisma/client"; @@ -5,8 +6,7 @@ const prisma = new PrismaClient(); async function main() { await seedUserOptions(); - // rome-ignore lint/nursery/noConsoleLog: - console.log("Seeding User Options complete!"); + await updateUserId(); } main() diff --git a/prisma/seeds/update-user-id.ts b/prisma/seeds/update-user-id.ts new file mode 100644 index 00000000..5e1dea65 --- /dev/null +++ b/prisma/seeds/update-user-id.ts @@ -0,0 +1,76 @@ +/* +Change user ID in database from int to 25char id +https://github.com/sinamics/ztnet/pull/191 +*/ + +import { PrismaClient } from "@prisma/client"; +import { createId } from "@paralleldrive/cuid2"; + +const prisma = new PrismaClient(); + +export async function updateUserId() { + const users = await prisma.user.findMany({ + include: { + network: true, + options: true, + userGroup: true, + }, + }); + + for (const user of users) { + if (Number.isInteger(Number(user.id.trim()))) { + const newId = createId(); + + // Create temporary unique email + const newEmail = `${user.email}_temp`; + + // Create a new user record with new ID + // Create new user + const { options, network, userGroup, ...otherUserFields } = user; + await prisma.user.create({ + data: { + ...otherUserFields, + id: newId, + email: newEmail, + }, + }); + + // Transfer networks to the new user + for (const net of network) { + await prisma.network.update({ + where: { nwid: net.nwid }, + data: { authorId: newId }, + }); + } + + // Transfer UserOptions to the new user, if they exist + if (options) { + // rome-ignore lint/correctness/noUnusedVariables: + const { id, userId, ...otherOptionsFields } = options; // Exclude id and userId + await prisma.userOptions.create({ + data: { ...otherOptionsFields, userId: newId }, + }); + } + // Transfer UserGroup to the new user, if it exists + if (userGroup) { + await prisma.user.update({ + where: { id: newId }, + data: { userGroupId: user.userGroupId }, + }); + } + + // Delete the old user record + await prisma.user.delete({ + where: { id: user.id }, + }); + + // Update back to original email + await prisma.user.update({ + where: { id: newId }, + data: { email: user.email }, + }); + } + } + // rome-ignore lint/nursery/noConsoleLog: + console.log("Seeding:: Updating user ID complete!"); +} diff --git a/prisma/seeds/user-option.seed.ts b/prisma/seeds/user-option.seed.ts index 54b7c620..76e28f9c 100644 --- a/prisma/seeds/user-option.seed.ts +++ b/prisma/seeds/user-option.seed.ts @@ -26,7 +26,6 @@ export async function seedUserOptions() { }); } } - // rome-ignore lint/nursery/noConsoleLog: - console.log("Seeding User Options complete!"); + console.log("Seeding:: User Options complete!"); } diff --git a/src/components/adminPage/users/table/accounts.tsx b/src/components/adminPage/users/table/accounts.tsx index cc89cabc..46d9f219 100644 --- a/src/components/adminPage/users/table/accounts.tsx +++ b/src/components/adminPage/users/table/accounts.tsx @@ -49,11 +49,11 @@ export const Accounts = () => { const columnHelper = createColumnHelper(); const columns = useMemo[]>( () => [ - columnHelper.accessor("id", { - header: () => {t("users.users.table.id")}, - id: "id", - minSize: 70, - }), + // columnHelper.accessor("id", { + // header: () => {t("users.users.table.id")}, + // id: "id", + // minSize: 70, + // }), columnHelper.accessor("name", { header: () => {t("users.users.table.memberName")}, id: "name", diff --git a/src/components/adminPage/users/userOptionsModal.tsx b/src/components/adminPage/users/userOptionsModal.tsx index 2a2cdf0a..377d785d 100644 --- a/src/components/adminPage/users/userOptionsModal.tsx +++ b/src/components/adminPage/users/userOptionsModal.tsx @@ -10,7 +10,7 @@ import { useTranslations } from "next-intl"; import UserIsActive from "./userIsActive"; interface Iprops { - userId: number; + userId: string; } const UserOptionsModal = ({ userId }: Iprops) => { diff --git a/src/components/adminPage/users/userRole.tsx b/src/components/adminPage/users/userRole.tsx index f76df002..c025198c 100644 --- a/src/components/adminPage/users/userRole.tsx +++ b/src/components/adminPage/users/userRole.tsx @@ -4,7 +4,6 @@ import React from "react"; import toast from "react-hot-toast"; import { ErrorData } from "~/types/errorHandling"; import { api } from "~/utils/api"; -// import { useModalStore } from "~/utils/store"; interface Iuser { user: Partial; @@ -42,7 +41,7 @@ const UserRole = ({ user }: Iuser) => { } }, }); - const dropDownHandler = (e: React.ChangeEvent, id: number) => { + const dropDownHandler = (e: React.ChangeEvent, id: string) => { let description = ""; if (e.target.value === "ADMIN") { diff --git a/src/components/adminPage/settings/apiToken.tsx b/src/components/userSettings/apiToken.tsx similarity index 85% rename from src/components/adminPage/settings/apiToken.tsx rename to src/components/userSettings/apiToken.tsx index 2de8cdf3..ffbb9c95 100644 --- a/src/components/adminPage/settings/apiToken.tsx +++ b/src/components/userSettings/apiToken.tsx @@ -10,12 +10,12 @@ import { useTranslations } from "next-intl"; const ApiLables = ({ tokens }) => { if (!Array.isArray(tokens) || !tokens) return null; - const t = useTranslations("admin"); + const t = useTranslations("userSettings"); - const { refetch } = api.admin.getApiToken.useQuery(); + const { refetch } = api.auth.getApiToken.useQuery(); const { callModal } = useModalStore((state) => state); - const { mutate: deleteToken } = api.admin.deleteApiToken.useMutation({ + const { mutate: deleteToken } = api.auth.deleteApiToken.useMutation({ onError: (error) => { if ((error.data as ErrorData)?.zodError) { const fieldErrors = (error.data as ErrorData)?.zodError.fieldErrors; @@ -83,8 +83,8 @@ const ApiLables = ({ tokens }) => { className="z-10 ml-4 h-4 w-4 cursor-pointer text-warning" onClick={() => { callModal({ - title: t("settings.restapi.modals.deleteToken.title"), - description: t("settings.restapi.modals.deleteToken.description"), + title: t("account.restapi.modals.deleteToken.title"), + description: t("account.restapi.modals.deleteToken.description"), yesAction: () => { deleteToken({ id: token.id, @@ -109,10 +109,10 @@ const ApiLables = ({ tokens }) => { const ApiToken = () => { const callModal = useModalStore((state) => state.callModal); - const t = useTranslations("admin"); - const { data: apiTokens, refetch } = api.admin.getApiToken.useQuery(); + const t = useTranslations("userSettings"); + const { data: apiTokens, refetch } = api.auth.getApiToken.useQuery(); - const { mutate: addToken } = api.admin.addApiToken.useMutation({ + const { mutate: addToken } = api.auth.addApiToken.useMutation({ onError: (error) => { if ((error.data as ErrorData)?.zodError) { const fieldErrors = (error.data as ErrorData)?.zodError.fieldErrors; @@ -138,14 +138,14 @@ const ApiToken = () => { rootFormClassName="flex flex-col space-y-2" size="sm" placeholder="" - buttonText={t("settings.restapi.buttons.submitToken")} + buttonText={t("account.restapi.buttons.submitToken")} fields={[ { name: "name", type: "text", elementType: "input", - placeholder: t("settings.restapi.inputFields.tokenName.placeholder"), - description: t("settings.restapi.inputFields.tokenName.label"), + placeholder: t("account.restapi.inputFields.tokenName.placeholder"), + description: t("account.restapi.inputFields.tokenName.label"), }, ]} submitHandler={(params) => @@ -160,15 +160,15 @@ const ApiToken = () => { content: ( toast.success("successfully copied")} + onCopy={() => toast.success("Token copied")} title="Copied to clipboard" >

- {t("settings.restapi.response.info")} + {t("account.restapi.response.info")}

- {t("settings.restapi.response.title")} + {t("account.restapi.response.title")}

diff --git a/src/cronTasks.ts b/src/cronTasks.ts index d1b82b9e..70c5bf90 100644 --- a/src/cronTasks.ts +++ b/src/cronTasks.ts @@ -5,7 +5,7 @@ import * as ztController from "~/utils/ztApi"; type FakeContext = { session: { user: { - id: number; + id: string; }; }; }; diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 376ad9fd..e4e4c975 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -272,30 +272,6 @@ "publicPages": { "sectionTitle": "Public Pages", "description": "Customize the text displayed on your login and registration pages to better align with your brand or provide specific instructions to your users." - }, - "restapi": { - "sectionTitle": "API Access Tokens", - "description": "API access tokens are used to access the ZTNET Public API.", - "inputFields": { - "tokenName": { - "label": "Add a meaningful name to your token", - "placeholder": "My awesome token" - } - }, - "buttons": { - "submitToken": "Generate Token", - "cancle": "Delete Token" - }, - "response": { - "title": "Click the token to Copy", - "info": "This token will only be shown once. Please copy it now!" - }, - "modals": { - "deleteToken": { - "title": "Delete Token", - "description": "Are you sure you want to delete this token? This cannot be undone." - } - } } }, "mail": { @@ -516,15 +492,29 @@ "repeatNewPasswordPlaceholder": "Repeat New Password", "role": "Role" }, - "networkSetting": { - "memberAnotations": "Member Anotations", - "showMarkerInTable": "Show marker in Table", - "showMarkerInTableDescription": "This will add a circle before the the member name with the anotation color.

You can still search the anotation if disabled.", - "addBackgroundColorInTable": "Add background color in table", - "addBackgroundColorInTableDescription": "This will add row background color based on the anotation color.

You can still search the anotation if disabled.", - "memberTableTitle": "Member Table", - "deAuthorizationWarningTitle": "Display warning on member De-Authorization", - "deAuthorizationWarningLabel": "Display a confirmation modal to prevent accidental de-authorizations." + "restapi": { + "sectionTitle": "API Access Tokens", + "description": "This allows you to generate an API token that you can use to authenticate and gain access to our application's API services. With a valid token, you can make requests to the API and interact with the application's features programmatically.", + "inputFields": { + "tokenName": { + "label": "Add a meaningful name to your token", + "placeholder": "My awesome token" + } + }, + "buttons": { + "submitToken": "Generate Token", + "cancle": "Delete Token" + }, + "response": { + "title": "Click the token to Copy", + "info": "This token will only be shown once. Please copy it now!" + }, + "modals": { + "deleteToken": { + "title": "Delete Token", + "description": "Are you sure you want to delete this token? This cannot be undone." + } + } }, "zerotierCentral": { "title": "Zerotier Central", @@ -542,6 +532,18 @@ "version": "Version", "developerMode": "developer mode" } + }, + "network": { + "annotations": { + "memberAnotations": "Member Annotations", + "showMarkerInTable": "Show marker in Table", + "showMarkerInTableDescription": "This will add a circle before the the member name with the anotation color.

You can still search the anotation if disabled.", + "addBackgroundColorInTable": "Add background color in table", + "addBackgroundColorInTableDescription": "This will add row background color based on the anotation color.

You can still search the anotation if disabled.", + "memberTableTitle": "Member Table", + "deAuthorizationWarningTitle": "Display warning on member De-Authorization", + "deAuthorizationWarningLabel": "Display a confirmation modal to prevent accidental de-authorizations." + } } } } \ No newline at end of file diff --git a/src/locales/es/common.json b/src/locales/es/common.json index e95b5d92..5227f272 100644 --- a/src/locales/es/common.json +++ b/src/locales/es/common.json @@ -272,30 +272,6 @@ "publicPages": { "sectionTitle": "Páginas Públicas", "description": "Personaliza el texto que se muestra en tus páginas de inicio de sesión y registro para alinearlo mejor con tu marca o proporcionar instrucciones específicas a tus usuarios." - }, - "restapi": { - "sectionTitle": "Tokens de Acceso a la API", - "description": "Los tokens de acceso a la API se utilizan para acceder a la API pública de ZTNET.", - "inputFields": { - "tokenName": { - "label": "Agrega un nombre significativo a tu token", - "placeholder": "Mi token asombroso" - } - }, - "buttons": { - "submitToken": "Generar Token", - "cancle": "Eliminar Token" - }, - "response": { - "title": "Haz clic en el token para copiarlo", - "info": "Este token solo se mostrará una vez. ¡Cópialo ahora!" - }, - "modals": { - "deleteToken": { - "title": "Eliminar toke", - "description": "¿Estás seguro de que quieres eliminar este token? Esto no se puede deshacer." - } - } } }, "mail": { @@ -516,15 +492,29 @@ "repeatNewPasswordPlaceholder": "Repetir Nueva Contraseña", "role": "Rol" }, - "networkSetting": { - "memberAnotations": "Anotaciones de miembro", - "showMarkerInTable": "Mostrar marcador en la tabla", - "showMarkerInTableDescription": "Esto agregará un círculo antes del nombre del miembro con el color de la anotación.

Todavía puedes buscar la anotación si está desactivada.", - "addBackgroundColorInTable": "Agregar color de fondo en la tabla", - "addBackgroundColorInTableDescription": "Esto agregará color de fondo a la fila según el color de la anotación.

Todavía puedes buscar la anotación si está desactivada.", - "memberTableTitle": "Tabla de Miembros", - "deAuthorizationWarningTitle": "Mostrar advertencia al desautorizar a un miembro", - "deAuthorizationWarningLabel": "Mostrar un modal de confirmación para evitar desautorizaciones accidentales." + "restapi": { + "sectionTitle": "Tokens de Acceso a la API", + "description": "Los tokens de acceso a la API se utilizan para acceder a la API pública de ZTNET.", + "inputFields": { + "tokenName": { + "label": "Agrega un nombre significativo a tu token", + "placeholder": "Mi token asombroso" + } + }, + "buttons": { + "submitToken": "Generar Token", + "cancle": "Eliminar Token" + }, + "response": { + "title": "Haz clic en el token para copiarlo", + "info": "Este token solo se mostrará una vez. ¡Cópialo ahora!" + }, + "modals": { + "deleteToken": { + "title": "Eliminar toke", + "description": "¿Estás seguro de que quieres eliminar este token? Esto no se puede deshacer." + } + } }, "zerotierCentral": { "title": "Zerotier Central", @@ -542,6 +532,18 @@ "version": "Versión", "developerMode": "modo de desarrollador" } + }, + "network": { + "annotations": { + "memberAnotations": "Anotaciones de miembro", + "showMarkerInTable": "Mostrar marcador en la tabla", + "showMarkerInTableDescription": "Esto agregará un círculo antes del nombre del miembro con el color de la anotación.

Todavía puedes buscar la anotación si está desactivada.", + "addBackgroundColorInTable": "Agregar color de fondo en la tabla", + "addBackgroundColorInTableDescription": "Esto agregará color de fondo a la fila según el color de la anotación.

Todavía puedes buscar la anotación si está desactivada.", + "memberTableTitle": "Tabla de Miembros", + "deAuthorizationWarningTitle": "Mostrar advertencia al desautorizar a un miembro", + "deAuthorizationWarningLabel": "Mostrar un modal de confirmación para evitar desautorizaciones accidentales." + } } } } \ No newline at end of file diff --git a/src/locales/no/common.json b/src/locales/no/common.json index dbdb44e7..9d8dd355 100644 --- a/src/locales/no/common.json +++ b/src/locales/no/common.json @@ -272,30 +272,6 @@ "publicPages": { "sectionTitle": "Offentlige Sider", "description": "Tilpass teksten som vises på innloggings- og registreringssidene dine for å bedre samsvare med merkevaren din eller gi spesifikke instruksjoner til brukerne dine." - }, - "restapi": { - "sectionTitle": "API-tilgangstokens", - "description": "API-tilgangstokens brukes for å få tilgang til ZTNETs offentlige API.", - "inputFields": { - "tokenName": { - "label": "Legg til et meningsfylt navn på tokenet ditt", - "placeholder": "Mitt fantastiske token" - } - }, - "buttons": { - "submitToken": "Generer token", - "cancle": "Slett token" - }, - "response": { - "title": "Klikk på tokenet for å kopiere det", - "info": "Dette tokenet vil bare vises en gang. Vennligst kopier det nå!" - }, - "modals": { - "deleteToken": { - "title": "Slett token", - "description": "Er du sikker på at du vil slette dette tokenet? Dette kan ikke angres." - } - } } }, "mail": { @@ -516,15 +492,29 @@ "repeatNewPasswordPlaceholder": "Gjenta Nytt Passord", "role": "Rolle" }, - "networkSetting": { - "memberAnotations": "Medlemsanmerkninger", - "showMarkerInTable": "Vis markør i tabellen", - "showMarkerInTableDescription": "Dette vil legge til en sirkel før medlemsnavnet med anmerkningsfargen.

Du kan fremdeles søke anmerkningen hvis deaktivert.", - "addBackgroundColorInTable": "Legg til bakgrunnsfarge i tabellen", - "addBackgroundColorInTableDescription": "Dette vil legge til radbakgrunnsfarge basert på anmerkningsfargen.

Du kan fremdeles søke anmerkningen hvis deaktivert.", - "memberTableTitle": "Medlems Liste", - "deAuthorizationWarningTitle": "Vis advarsel ved fjerning av medlemsautorisasjon", - "deAuthorizationWarningLabel": "Vis en bekreftelsesmodal for å forhindre utilsiktet fjerning av autorisasjoner." + "restapi": { + "sectionTitle": "API-tilgangstokens", + "description": "API-tilgangstokens brukes for å få tilgang til ZTNETs offentlige API.", + "inputFields": { + "tokenName": { + "label": "Legg til et meningsfylt navn på tokenet ditt", + "placeholder": "Mitt fantastiske token" + } + }, + "buttons": { + "submitToken": "Generer token", + "cancle": "Slett token" + }, + "response": { + "title": "Klikk på tokenet for å kopiere det", + "info": "Dette tokenet vil bare vises en gang. Vennligst kopier det nå!" + }, + "modals": { + "deleteToken": { + "title": "Slett token", + "description": "Er du sikker på at du vil slette dette tokenet? Dette kan ikke angres." + } + } }, "zerotierCentral": { "title": "Zerotier Central", @@ -542,6 +532,18 @@ "version": "Versjon", "developerMode": "utviklermodus" } + }, + "network": { + "annotations": { + "memberAnotations": "Medlemsanmerkninger", + "showMarkerInTable": "Vis markør i tabellen", + "showMarkerInTableDescription": "Dette vil legge til en sirkel før medlemsnavnet med anmerkningsfargen.

Du kan fremdeles søke anmerkningen hvis deaktivert.", + "addBackgroundColorInTable": "Legg til bakgrunnsfarge i tabellen", + "addBackgroundColorInTableDescription": "Dette vil legge til radbakgrunnsfarge basert på anmerkningsfargen.

Du kan fremdeles søke anmerkningen hvis deaktivert.", + "memberTableTitle": "Medlems Liste", + "deAuthorizationWarningTitle": "Vis advarsel ved fjerning av medlemsautorisasjon", + "deAuthorizationWarningLabel": "Vis en bekreftelsesmodal for å forhindre utilsiktet fjerning av autorisasjoner." + } } } } \ No newline at end of file diff --git a/src/locales/zh/common.json b/src/locales/zh/common.json index 3bc83feb..34847980 100644 --- a/src/locales/zh/common.json +++ b/src/locales/zh/common.json @@ -272,30 +272,6 @@ "publicPages": { "sectionTitle": "公共页面", "description": "自定义登录和注册页面上显示的文本,以更好地与您的品牌保持一致或向您的用户提供具体说明。" - }, - "restapi": { - "sectionTitle": "API访问令牌", - "description": "API访问令牌用于访问ZTNET公共API。", - "inputFields": { - "tokenName": { - "label": "为您的令牌添加有意义的名称", - "placeholder": "我的超棒令牌" - } - }, - "buttons": { - "submitToken": "生成令牌", - "cancle": "删除令牌" - }, - "response": { - "title": "点击令牌以复制", - "info": "此令牌只会显示一次。请立即复制它!" - }, - "modals": { - "deleteToken": { - "title": "删除令牌", - "description": "您确定要删除此令牌吗?此操作无法撤消。" - } - } } }, "mail": { @@ -516,15 +492,29 @@ "repeatNewPasswordPlaceholder": "重复新密码", "role": "角色" }, - "networkSetting": { - "memberAnotations": "成员注释", - "showMarkerInTable": "在表格中显示标记", - "showMarkerInTableDescription": "这将在成员名称前添加一个带有注释颜色的圆圈。

如果禁用,您仍然可以搜索注释。", - "addBackgroundColorInTable": "在表格中添加背景颜色", - "addBackgroundColorInTableDescription": "这将根据注释颜色添加行背景颜色。

如果禁用,您仍然可以搜索注释。", - "memberTableTitle": "成员表格", - "deAuthorizationWarningTitle": "在取消成员授权时显示警告", - "deAuthorizationWarningLabel": "显示确认模态框以防止意外取消授权。" + "restapi": { + "sectionTitle": "API访问令牌", + "description": "API访问令牌用于访问ZTNET公共API。", + "inputFields": { + "tokenName": { + "label": "为您的令牌添加有意义的名称", + "placeholder": "我的超棒令牌" + } + }, + "buttons": { + "submitToken": "生成令牌", + "cancle": "删除令牌" + }, + "response": { + "title": "点击令牌以复制", + "info": "此令牌只会显示一次。请立即复制它!" + }, + "modals": { + "deleteToken": { + "title": "删除令牌", + "description": "您确定要删除此令牌吗?此操作无法撤消。" + } + } }, "zerotierCentral": { "title": "Zerotier Central", @@ -542,6 +532,18 @@ "version": "版本", "developerMode": "开发者模式" } + }, + "network": { + "annotations": { + "memberAnotations": "成员注释", + "showMarkerInTable": "在表格中显示标记", + "showMarkerInTableDescription": "这将在成员名称前添加一个带有注释颜色的圆圈。

如果禁用,您仍然可以搜索注释。", + "addBackgroundColorInTable": "在表格中添加背景颜色", + "addBackgroundColorInTableDescription": "这将根据注释颜色添加行背景颜色。

如果禁用,您仍然可以搜索注释。", + "memberTableTitle": "成员表格", + "deAuthorizationWarningTitle": "在取消成员授权时显示警告", + "deAuthorizationWarningLabel": "显示确认模态框以防止意外取消授权。" + } } } } \ No newline at end of file diff --git a/src/pages/admin/settings/index.tsx b/src/pages/admin/settings/index.tsx index 0cb77275..7461692a 100644 --- a/src/pages/admin/settings/index.tsx +++ b/src/pages/admin/settings/index.tsx @@ -5,8 +5,6 @@ import { LayoutAdminAuthenticated } from "~/components/layouts/layout"; import { ErrorData, ZodErrorFieldErrors } from "~/types/errorHandling"; import { api } from "~/utils/api"; import { useTranslations } from "next-intl"; -import ApiToken from "~/components/adminPage/settings/apiToken"; -import Link from "next/link"; const Settings = () => { const t = useTranslations("admin"); @@ -102,26 +100,6 @@ const Settings = () => { />

-
-

{t("settings.restapi.sectionTitle")}

-
-
-

- {t("settings.restapi.description")} -
- - https://ztnet.network/Rest%20Api/ztnet-web-api - -

-
-
- -
-
); }; diff --git a/src/pages/api/v1/network/index.ts b/src/pages/api/v1/network/index.ts new file mode 100644 index 00000000..92073a7b --- /dev/null +++ b/src/pages/api/v1/network/index.ts @@ -0,0 +1,113 @@ +import { TRPCError } from "@trpc/server"; +import { getHTTPStatusCodeFromError } from "@trpc/server/http"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { createNetworkService } from "~/server/api/services/networkService"; +import { prisma } from "~/server/db"; +import { decryptAndVerifyToken } from "~/utils/encryption"; +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 createNetworkHandler( + 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_userNetworks(req, res); + break; + case "POST": + await POST_createNewNetwork(req, res); + break; + default: // Method Not Allowed + res.status(405).end(); + break; + } +} + +const POST_createNewNetwork = async (req: NextApiRequest, res: NextApiResponse) => { + const apiKey = req.headers["x-ztnet-auth"] as string; + + let decryptedData: { userId: string; name: string }; + + // If there are users, verify the API key + try { + decryptedData = await decryptAndVerifyToken({ apiKey }); + } catch (error) { + return res.status(401).json({ error: error.message }); + } + const { name } = req.body; + + const ctx = { + session: { + user: { + id: decryptedData.userId as string, + }, + }, + prisma, + }; + + const newNetworkId = await createNetworkService({ + ctx, + input: { central: false, name }, + }); + + return res.status(200).json(newNetworkId); +}; + +const GET_userNetworks = async (req: NextApiRequest, res: NextApiResponse) => { + const apiKey = req.headers["x-ztnet-auth"] as string; + + let decryptedData: { userId: string; name?: string }; + // If there are users, verify the API key + + try { + decryptedData = await decryptAndVerifyToken({ apiKey }); + } catch (error) { + return res.status(401).json({ error: error.message }); + } + + try { + const user = await prisma.user.findFirst({ + where: { + id: decryptedData.userId, + }, + select: { + network: { + select: { + nwid: true, + }, + }, + }, + }); + + // create array of only the nwid values + const nwids = user?.network.map((nw) => nw.nwid); + + return res.status(200).json(nwids); + } catch (cause) { + if (cause instanceof TRPCError) { + const httpCode = getHTTPStatusCodeFromError(cause); + try { + const parsedErrors = JSON.parse(cause.message); + return res.status(httpCode).json({ cause: parsedErrors }); + } catch (_error) { + return res.status(httpCode).json({ error: cause.message }); + } + } + return res.status(500).json({ message: "Internal server error" }); + } +}; diff --git a/src/pages/api/v1/user/index.ts b/src/pages/api/v1/user/index.ts index c1c2b7fa..a10a38af 100644 --- a/src/pages/api/v1/user/index.ts +++ b/src/pages/api/v1/user/index.ts @@ -13,7 +13,7 @@ const limiter = rateLimit({ uniqueTokenPerInterval: 500, // Max 500 users per second }); -const REQUEST_PR_MINUTE = 10; +const REQUEST_PR_MINUTE = 50; export default async function createUserHandler( req: NextApiRequest, @@ -22,19 +22,32 @@ export default async function createUserHandler( try { await limiter.check(res, REQUEST_PR_MINUTE, "CREATE_USER_CACHE_TOKEN"); // 10 requests per minute } catch { - res.status(429).json({ error: "Rate limit exceeded" }); + return res.status(429).json({ error: "Rate limit exceeded" }); } - if (req.method !== "POST") return res.status(405).end(); // Method Not Allowed + // create a switch based on the HTTP method + switch (req.method) { + case "POST": + await POST_createUser(req, res); + break; + default: // Method Not Allowed + res.status(405).end(); + break; + } +} + +const POST_createUser = async (req: NextApiRequest, res: NextApiResponse) => { const apiKey = req.headers["x-ztnet-auth"] as string; + const NEEDS_ADMIN = true; + // Count the number of users in database const userCount = await prisma.user.count(); if (userCount > 0) { // If there are users, verify the API key try { - await decryptAndVerifyToken(apiKey); + await decryptAndVerifyToken({ apiKey, requireAdmin: NEEDS_ADMIN }); } catch (error) { return res.status(401).json({ error: error.message }); } @@ -47,6 +60,23 @@ export default async function createUserHandler( // get data from the post request const { email, password, name, expiresAt } = req.body; + if (userCount === 0 && expiresAt !== undefined) { + return res.status(400).json({ message: "Cannot add expiresAt for Admin user!" }); + } + // Check if expiresAt is a valid date + if (expiresAt !== undefined) { + try { + const date = new Date(expiresAt); + const isoString = date.toISOString(); + + if (expiresAt !== isoString) { + return res.status(400).json({ message: "Invalid expiresAt date" }); + } + } catch (error) { + return res.status(400).json({ message: error.message }); + } + } + try { const user = await caller.auth.register({ email: email as string, @@ -63,9 +93,9 @@ export default async function createUserHandler( const parsedErrors = JSON.parse(cause.message); return res.status(httpCode).json({ cause: parsedErrors }); } catch (_error) { - return res.status(httpCode).json({ error: cause.message }); + return res.status(httpCode).json({ error: cause.message.trim() }); } } - res.status(500).json({ message: "Internal server error" }); + return res.status(500).json({ message: "Internal server error" }); } -} +}; diff --git a/src/pages/user-settings/account/index.tsx b/src/pages/user-settings/account/index.tsx index d2ecc47e..38aa83e2 100644 --- a/src/pages/user-settings/account/index.tsx +++ b/src/pages/user-settings/account/index.tsx @@ -7,6 +7,8 @@ import InputField from "~/components/elements/inputField"; import { useRouter } from "next/router"; import { useTranslations } from "next-intl"; import { globalSiteVersion } from "~/utils/global"; +import Link from "next/link"; +import ApiToken from "~/components/userSettings/apiToken"; const languageNames = { en: "English", @@ -38,13 +40,13 @@ const Account = () => { } return ( -
-
+
+

{t("account.accountSettings.title").toUpperCase()}

-
+
{
+
+
+
+ {t("account.restapi.sectionTitle")} +
+
+
+

+ {t("account.restapi.description")} +
+ + https://ztnet.network/Rest%20Api/ztnet-web-api + +

+
+
+ +
+
+ +
- {t("account.zerotierCentral.title").toUpperCase()}{" "} + {t("account.zerotierCentral.title").toUpperCase()}
BETA
-
+

{t.rich("account.zerotierCentral.description", { br: () =>
, @@ -222,7 +249,9 @@ const Account = () => { />

-

+

+
+

{t("account.accountPreferences.title")}

@@ -244,8 +273,13 @@ const Account = () => { ))}
+
+ +
-

{t("account.application.title")}

+

+ {t("account.application.title")} +

{t("account.application.version")}

diff --git a/src/pages/user-settings/network/index.tsx b/src/pages/user-settings/network/index.tsx index 29dfe8a4..0333acc9 100644 --- a/src/pages/user-settings/network/index.tsx +++ b/src/pages/user-settings/network/index.tsx @@ -5,9 +5,9 @@ import { useTranslations } from "use-intl"; import { User, UserOptions } from "@prisma/client"; interface UserExtended extends User { - options: UserOptions & { - deAuthorizeWarning: boolean; - }; + options: UserOptions & { + deAuthorizeWarning: boolean; + }; } const UserNetworkSetting = () => { @@ -20,14 +20,14 @@ const UserNetworkSetting = () => {

- {t("account.networkSetting.memberAnotations")} + {t("network.annotations.memberAnotations")}

-

{t("account.networkSetting.showMarkerInTable")}

+

{t("network.annotations.showMarkerInTable")}

- {t.rich("account.networkSetting.showMarkerInTableDescription", { + {t.rich("network.annotations.showMarkerInTableDescription", { br: () =>
, })}

@@ -49,10 +49,10 @@ const UserNetworkSetting = () => {

- {t("account.networkSetting.addBackgroundColorInTable")} + {t("network.annotations.addBackgroundColorInTable")}

- {t.rich("account.networkSetting.addBackgroundColorInTableDescription", { + {t.rich("network.annotations.addBackgroundColorInTableDescription", { br: () =>
, })}

@@ -74,16 +74,16 @@ const UserNetworkSetting = () => {

- {t("account.networkSetting.memberTableTitle")} + {t("network.annotations.memberTableTitle")}

- {t("account.networkSetting.deAuthorizationWarningTitle")} + {t("network.annotations.deAuthorizationWarningTitle")}

- {t("account.networkSetting.deAuthorizationWarningLabel")} + {t("network.annotations.deAuthorizationWarningLabel")}

{ if (ctx.session.user.id === input.id) { throwError("You can't delete your own account"); } - if (input.id === 1) { - throwError("You can't delete the user who created the first account"); + + // check if user is admin user + const user = await ctx.prisma.user.findUnique({ + where: { + id: input.id, + }, + }); + + if (user.role === "ADMIN") { + throwError("You can't delete admin users"); } + // get user networks const userNetworks = await ctx.prisma.network.findMany({ where: { @@ -102,7 +106,7 @@ export const adminRouter = createTRPCRouter({ getUser: adminRoleProtectedRoute .input( z.object({ - userId: z.number(), + userId: z.string(), }), ) .query(async ({ ctx, input }) => { @@ -269,7 +273,7 @@ export const adminRouter = createTRPCRouter({ message: "Role is not valid", path: ["role"], }), - id: z.number(), + id: z.string(), }), ) .mutation(async ({ ctx, input }) => { @@ -636,7 +640,7 @@ export const adminRouter = createTRPCRouter({ // Use upsert to either update or create a new userGroup return await ctx.prisma.userGroup.upsert({ where: { - id: input.id || -1, // If no ID is provided, it assumes -1 which likely doesn't exist (assuming positive autoincrementing IDs) + id: input.id || -1, }, create: { name: input.groupName, @@ -718,7 +722,7 @@ export const adminRouter = createTRPCRouter({ assignUserGroup: adminRoleProtectedRoute .input( z.object({ - userid: z.number(), + userid: z.string(), userGroupId: z.string().nullable(), // Allow null value for userGroupId }), ) @@ -1041,48 +1045,4 @@ export const adminRouter = createTRPCRouter({ } } }), - getApiToken: adminRoleProtectedRoute.query(async ({ ctx }) => { - return await ctx.prisma.aPIToken.findMany({ - where: { - userId: ctx.session.user.id, - }, - }); - }), - addApiToken: adminRoleProtectedRoute - .input( - z.object({ - name: z.string().min(5).max(50), - }), - ) - .mutation(async ({ ctx, input }) => { - const token_conent: string = JSON.stringify({ - name: input.name, - userId: ctx.session.user.id, - }); - - const token_hash = encrypt(token_conent, generateInstanceSecret(API_TOKEN_SECRET)); - const token = await ctx.prisma.aPIToken.create({ - data: { - token: token_hash, - name: input.name, - userId: ctx.session.user.id, - }, - }); - return token; - }), - - deleteApiToken: adminRoleProtectedRoute - .input( - z.object({ - id: z.number(), - }), - ) - .mutation(async ({ ctx, input }) => { - return await ctx.prisma.aPIToken.delete({ - where: { - id: input.id, - userId: ctx.session.user.id, - }, - }); - }), }); diff --git a/src/server/api/routers/authRouter.ts b/src/server/api/routers/authRouter.ts index 768e30ac..851dad97 100644 --- a/src/server/api/routers/authRouter.ts +++ b/src/server/api/routers/authRouter.ts @@ -17,6 +17,7 @@ import { } from "~/utils/mail"; import ejs from "ejs"; import * as ztController from "~/utils/ztApi"; +import { API_TOKEN_SECRET, encrypt, generateInstanceSecret } from "~/utils/encryption"; // This regular expression (regex) is used to validate a password based on the following criteria: // - The password must be at least 6 characters long. @@ -428,7 +429,7 @@ export const authRouter = createTRPCRouter({ try { interface IJwt { - id: number; + id: string; token: string; } const { id } = jwt.decode(token) as IJwt; @@ -575,4 +576,48 @@ export const authRouter = createTRPCRouter({ return updated; }), + getApiToken: protectedProcedure.query(async ({ ctx }) => { + return await ctx.prisma.aPIToken.findMany({ + where: { + userId: ctx.session.user.id, + }, + }); + }), + addApiToken: protectedProcedure + .input( + z.object({ + name: z.string().min(5).max(50), + }), + ) + .mutation(async ({ ctx, input }) => { + const token_content: string = JSON.stringify({ + name: input.name, + userId: ctx.session.user.id, + }); + + const token_hash = encrypt(token_content, generateInstanceSecret(API_TOKEN_SECRET)); + const token = await ctx.prisma.aPIToken.create({ + data: { + token: token_hash, + name: input.name, + userId: ctx.session.user.id, + }, + }); + return token; + }), + + deleteApiToken: protectedProcedure + .input( + z.object({ + id: z.number(), + }), + ) + .mutation(async ({ ctx, input }) => { + return await ctx.prisma.aPIToken.delete({ + where: { + id: input.id, + userId: ctx.session.user.id, + }, + }); + }), }); diff --git a/src/server/api/routers/networkRouter.ts b/src/server/api/routers/networkRouter.ts index f8659ea4..b51a9bf0 100644 --- a/src/server/api/routers/networkRouter.ts +++ b/src/server/api/routers/networkRouter.ts @@ -1,12 +1,7 @@ import { z } from "zod"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; import { IPv4gen } from "~/utils/IPv4gen"; -import { - type Config, - adjectives, - animals, - uniqueNamesGenerator, -} from "unique-names-generator"; +import { type Config, adjectives, animals } from "unique-names-generator"; import * as ztController from "~/utils/ztApi"; import { enrichMembers, @@ -17,18 +12,15 @@ import { import { Address4, Address6 } from "ip-address"; import RuleCompiler from "~/utils/rule-compiler"; -import { - throwError, - type APIError, - CustomLimitError, -} from "~/server/helpers/errorHandler"; +import { throwError, type APIError } from "~/server/helpers/errorHandler"; import { createTransporter, inviteUserTemplate, sendEmail } from "~/utils/mail"; import ejs from "ejs"; import { type TagsByName, type NetworkEntity } from "~/types/local/network"; import { type CapabilitiesByName } from "~/types/local/member"; import { type CentralNetwork } from "~/types/central/network"; +import { createNetworkService } from "../services/networkService"; -const customConfig: Config = { +export const customConfig: Config = { dictionaries: [adjectives, animals], separator: "-", length: 2, @@ -587,75 +579,9 @@ export const networkRouter = createTRPCRouter({ central: z.boolean().optional().default(false), }), ) - .mutation(async ({ ctx, input }) => { - try { - // 1. Fetch the user with its related UserGroup - const userWithGroup = await ctx.prisma.user.findUnique({ - where: { id: ctx.session.user.id }, - select: { - userGroup: true, - }, - }); - - if (userWithGroup?.userGroup) { - // 2. Fetch the current number of networks linked to the user - const currentNetworksCount = await ctx.prisma.network.count({ - where: { authorId: ctx.session.user.id }, - }); - - // Check against the defined limit - const networkLimit = userWithGroup.userGroup.maxNetworks; - if (currentNetworksCount >= networkLimit) { - throw new CustomLimitError( - "You have reached the maximum number of networks allowed for your user group.", - ); - } - } - - // Generate ipv4 address, cidr, start & end - const ipAssignmentPools = IPv4gen(null); - - // Generate adjective and noun word - const networkName: string = uniqueNamesGenerator(customConfig); - - // Create ZT network - const newNw = await ztController.network_create( - ctx, - networkName, - ipAssignmentPools, - input.central, - ); - - if (input.central) return newNw; - - // Store the created network in the database - const updatedUser = await ctx.prisma.user.update({ - where: { - id: ctx.session.user.id, - }, - data: { - network: { - create: { - name: newNw.name, - nwid: newNw.nwid, - }, - }, - }, - select: { - network: true, - }, - }); - return updatedUser; - } catch (err: unknown) { - if (err instanceof CustomLimitError) { - throwError(err.message); - } else if (err instanceof Error) { - console.error(err); - throwError("Could not create network! Please try again"); - } else { - throwError("An unknown error occurred"); - } - } + .mutation(async (props) => { + // abstracted due to pages/api/v1/network/index.ts + await createNetworkService(props); }), setFlowRule: protectedProcedure .input( diff --git a/src/server/api/services/authService.ts b/src/server/api/services/authService.ts index 300e0f22..8d82b170 100644 --- a/src/server/api/services/authService.ts +++ b/src/server/api/services/authService.ts @@ -32,7 +32,7 @@ interface Ivalidate { token: string; } interface IJwt { - id: number; + id: string; token: string; } /** diff --git a/src/server/api/services/networkService.ts b/src/server/api/services/networkService.ts new file mode 100644 index 00000000..14bd0480 --- /dev/null +++ b/src/server/api/services/networkService.ts @@ -0,0 +1,78 @@ +import { uniqueNamesGenerator } from "unique-names-generator"; +import { CustomLimitError, throwError } from "~/server/helpers/errorHandler"; +import { IPv4gen } from "~/utils/IPv4gen"; +import { customConfig } from "../routers/networkRouter"; +import * as ztController from "~/utils/ztApi"; + +export const createNetworkService = async ({ ctx, input }) => { + try { + // 1. Fetch the user with its related UserGroup + const userWithGroup = await ctx.prisma.user.findUnique({ + where: { id: ctx.session.user.id }, + select: { + userGroup: true, + }, + }); + + if (userWithGroup?.userGroup) { + // 2. Fetch the current number of networks linked to the user + const currentNetworksCount = await ctx.prisma.network.count({ + where: { authorId: ctx.session.user.id }, + }); + + // Check against the defined limit + const networkLimit = userWithGroup.userGroup.maxNetworks; + if (currentNetworksCount >= networkLimit) { + throw new CustomLimitError( + "You have reached the maximum number of networks allowed for your user group.", + ); + } + } + + // Generate ipv4 address, cidr, start & end + const ipAssignmentPools = IPv4gen(null); + + if (!input?.name) { + // Generate adjective and noun word + input.name = uniqueNamesGenerator(customConfig); + } + + // Create ZT network + const newNw = await ztController.network_create( + ctx, + input.name, + ipAssignmentPools, + input.central, + ); + + if (input.central) return newNw; + + // Store the created network in the database + await ctx.prisma.user.update({ + where: { + id: ctx.session.user.id, + }, + data: { + network: { + create: { + name: newNw.name, + nwid: newNw.nwid, + }, + }, + }, + select: { + network: true, + }, + }); + return newNw; + } catch (err: unknown) { + if (err instanceof CustomLimitError) { + throwError(err.message); + } else if (err instanceof Error) { + console.error(err); + throwError("Could not create network! Please try again"); + } else { + throwError("An unknown error occurred"); + } + } +}; diff --git a/src/server/auth.ts b/src/server/auth.ts index a1f5fba5..ccd5eca3 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -26,7 +26,7 @@ declare module "next-auth" { } interface User { - id?: number; + id?: string; name: string; role: string; // ...other properties @@ -124,6 +124,12 @@ export const authOptions: NextAuthOptions = { lastseen: true, }, }); + + // Number(user.id.trim()) checks if the user session has the old int as the User id + if (Number.isInteger(Number(token.id))) { + return undefined; + } + // session update => https://github.com/nextauthjs/next-auth/discussions/3941 // verify that name has at least one character if (typeof session.update.name === "string") { @@ -137,16 +143,14 @@ export const authOptions: NextAuthOptions = { // verify that email is valid if (typeof session.update.email === "string") { // eslint-disable-next-line @typescript-eslint/no-unsafe-call - // if (!session.update.email.includes("@")) { - // throw new Error("Email must be a valid email address."); - // } + token.email = session.update.email; } // update user with new values await prisma.user.update({ where: { - id: token.id as number, + id: token.id as string, }, data: { email: session.update.email || user.email, @@ -177,7 +181,8 @@ export const authOptions: NextAuthOptions = { where: { id: token.id }, }); - if (!user || !user.isActive) { + // Number(user.id.trim()) checks if the user session has the old int as the User id + if (!user || !user.isActive || Number.isInteger(Number(token.id))) { // If the user does not exist, set user to null return { ...session, user: null }; } diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index db82f495..adbb8d34 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -56,11 +56,19 @@ export const decrypt = (text: string, secret: Buffer) => { }; type DecryptedTokenData = { - userId: number; + userId: string; name: string; }; -export async function decryptAndVerifyToken(apiKey: string): Promise { +type VerifyToken = { + apiKey: string; + requireAdmin?: boolean; +}; + +export async function decryptAndVerifyToken({ + apiKey, + requireAdmin = false, +}: VerifyToken): Promise { // Check if API key is provided if (!apiKey) { throw new Error("API key missing"); @@ -73,11 +81,11 @@ export async function decryptAndVerifyToken(apiKey: string): Promise