Skip to content

Commit

Permalink
changed id collumn from int to string. included id in token. !Breakin…
Browse files Browse the repository at this point in the history
…g Change
  • Loading branch information
sinamics committed Mar 15, 2024
1 parent 7cec18e commit 73692b2
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 29 deletions.

This file was deleted.

13 changes: 13 additions & 0 deletions prisma/migrations/20240315064831_api_token_extended/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
Warnings:
- The primary key for the `APIToken` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- AlterTable
ALTER TABLE "APIToken" DROP CONSTRAINT "APIToken_pkey",
ADD COLUMN "apiAuthorizationType" JSONB NOT NULL DEFAULT '["PERSONAL"]',
ALTER COLUMN "id" DROP DEFAULT,
ALTER COLUMN "id" SET DATA TYPE TEXT,
ADD CONSTRAINT "APIToken_pkey" PRIMARY KEY ("id");
DROP SEQUENCE "APIToken_id_seq";
2 changes: 1 addition & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ model UserGroup {
}

model APIToken {
id Int @id @default(autoincrement())
id String @id @default(cuid())
name String
token String @unique
userId String
Expand Down
6 changes: 4 additions & 2 deletions src/components/userSettings/apiToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ const ApiLables = ({ tokens }) => {
return (
<div
key={token.id}
className={cn("badge badge-lg rounded-md flex items-center badge-primary")}
className={cn("badge badge-lg rounded-md flex items-center badge-primary", {
"bg-error": !token.isActive,
})}
>
<div
onClick={() => {
Expand Down Expand Up @@ -87,7 +89,7 @@ const ApiLables = ({ tokens }) => {
}}
className="cursor-pointer"
>
<p>{token.name}</p>
<p>{!token.isActive ? `Expired: ${token.name}` : token.name}</p>
</div>

<div>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/user-settings/account/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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";
import GenerateApiToken from "~/components/userSettings/apiToken";
import { languageNames, supportedLocales } from "~/locales/lang";
import ApplicationFontSize from "~/components/userSettings/fontSize";

Expand Down Expand Up @@ -176,7 +176,7 @@ const Account = () => {
</p>
</div>
<div className="space-y-5">
<ApiToken />
<GenerateApiToken />
</div>
</div>

Expand Down
70 changes: 53 additions & 17 deletions src/server/api/routers/authRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,11 +630,29 @@ export const authRouter = createTRPCRouter({
return updated;
}),
getApiToken: protectedProcedure.query(async ({ ctx }) => {
return await ctx.prisma.aPIToken.findMany({
const tokens = await ctx.prisma.aPIToken.findMany({
where: {
userId: ctx.session.user.id,
},
orderBy: {
createdAt: "asc",
},
});

// if expiresAt is < now, set isActive to false. use for of loop to avoid async issues
for (const token of tokens) {
if (token.expiresAt) {
await ctx.prisma.aPIToken.update({
where: {
id: token.id,
},
data: {
isActive: token.expiresAt > new Date(),
},
});
}
}
return tokens;
}),
addApiToken: protectedProcedure
.input(
Expand All @@ -646,37 +664,55 @@ export const authRouter = createTRPCRouter({
)
.mutation(async ({ ctx, input }) => {
try {
// generate daysToExpire date. If "never" is selected or a empty string, the token will never expire.
// generate daysToExpire date. If "never" is selected or an empty string, the token will never expire.
const daysToExpire = parseInt(input.daysToExpire);
const expiresAt =
typeof daysToExpire === "number" && daysToExpire > 0 ? new Date() : null;
if (expiresAt) {
expiresAt.setDate(expiresAt.getDate() + parseInt(input.daysToExpire));
let expiresAt: Date | null = new Date();
if (!Number.isNaN(daysToExpire) && daysToExpire > 0) {
expiresAt.setDate(expiresAt.getDate() + daysToExpire);
} else {
expiresAt = null; // Token never expires
}

// token factory
const token_content: string = JSON.stringify({
const tokenContent = JSON.stringify({
userId: ctx.session.user.id,
expiresAt,
apiAuthorizationType: input.apiAuthorizationType,
});

// hash token
const token_hash = encrypt(
token_content,
generateInstanceSecret(API_TOKEN_SECRET),
);
const tokenHash = encrypt(tokenContent, generateInstanceSecret(API_TOKEN_SECRET));

// store token in database
// store token in database with tokenHash
const token = await ctx.prisma.aPIToken.create({
data: {
token: token_hash,
token: tokenHash,
name: input.name,
apiAuthorizationType: input.apiAuthorizationType,
userId: ctx.session.user.id,
expiresAt,
},
});
return token;

// Add the database token ID to the token hash for reference
const tokenId = token.id.toString(); // Just in case the token id is not a string ( old db structure )
const tokenWithIdContent = JSON.stringify({
...JSON.parse(tokenContent),
tokenId,
});

// hash token with token id
const tokenWithIdHash = encrypt(
tokenWithIdContent,
generateInstanceSecret(API_TOKEN_SECRET),
);

// Update the token in the database with the new hash that includes the tokenId
const updatedToken = await ctx.prisma.aPIToken.update({
where: { id: token.id },
data: { token: tokenWithIdHash },
});

return updatedToken;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
Expand All @@ -688,13 +724,13 @@ export const authRouter = createTRPCRouter({
deleteApiToken: protectedProcedure
.input(
z.object({
id: z.number(),
id: z.union([z.string(), z.number()]),
}),
)
.mutation(async ({ ctx, input }) => {
return await ctx.prisma.aPIToken.delete({
where: {
id: input.id,
id: input.id.toString(),
userId: ctx.session.user.id,
},
});
Expand Down
29 changes: 24 additions & 5 deletions src/utils/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type DecryptedTokenData = {
userId: string;
name: string;
apiAuthorizationType: AuthorizationType;
expiresAt: string;
tokenId: string;
};

type VerifyToken = {
Expand Down Expand Up @@ -93,7 +93,11 @@ export async function decryptAndVerifyToken({
}

// Validate the decrypted data structure (add more validations as necessary)
if (!decryptedData.userId || typeof decryptedData.userId !== "string") {
if (
!decryptedData.userId ||
typeof decryptedData.userId !== "string" ||
!decryptedData.tokenId
) {
throw new Error("Invalid token structure");
}

Expand All @@ -105,9 +109,24 @@ export async function decryptAndVerifyToken({
throw new Error("Invalid Authorization Type");
}

// check if the token has expired. if decryptedData.expiresAt is null or undefined, it means the token never expires
if (decryptedData.expiresAt) {
const expiresAt = new Date(decryptedData.expiresAt);
// get the token from the database
const token = await prisma.aPIToken.findUnique({
where: {
id: decryptedData.tokenId,
userId: decryptedData.userId,
},
select: {
expiresAt: true,
},
});

if (!token) {
throw new Error("Invalid token");
}

// check if the token is expired
if (token.expiresAt) {
const expiresAt = new Date(token.expiresAt);
if (expiresAt < new Date()) {
throw new Error("Token expired");
}
Expand Down

3 comments on commit 73692b2

@tinola
Copy link
Contributor

@tinola tinola commented on 73692b2 Mar 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got trouble...


...
28 migrations found in prisma/migrations

Applying migration `20240315064831_api_token_extended`
Error: P3018

A migration failed to apply. New migrations cannot be applied before the error is recovered from. Read more about how to resolve migration issues in a production database: https://pris.ly/d/migrate-resolve

Migration name: 20240315064831_api_token_extended

Database error code: 42701

Database error:
ERROR: column "apiAuthorizationType" of relation "APIToken" already exists

DbError { severity: "ERROR", parsed_severity: Some(Error), code: SqlState(E42701), message: "column \"apiAuthorizationType\" of relation \"APIToken\" already exists", detail: None, hint: None, position: None, where_: None, schema: None, table: None, column: None, datatype: None, constraint: None, file: Some("tablecmds.c"), line: Some(7249), routine: Some("check_for_column_name_collision") }

To resolve this error:

  1. dropped column "apiAuthorizationType" in database (I used phpPgAdmin)
  2. npx prisma migrate resolve --rolled-back "20240315064831_api_token_extended"
  3. npx prisma migrate deploy
  4. npx prisma db seed

After that a piece of output:

28 migrations found in prisma/migrations

Applying migration `20240315064831_api_token_extended`

The following migration(s) have been applied:

migrations/
  └─ 20240315064831_api_token_extended/
    └─ migration.sql
...


@sinamics
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, that is expected as you are deploying multiple times from main branch.
Main is not a release, and the migration files could change, hence the migration error you have.

this error will not occur when updating from 0.5.11 to 0.6.0 release.

@tinola
Copy link
Contributor

@tinola tinola commented on 73692b2 Mar 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx for clarification.

Please sign in to comment.