fix: user uuid (#355)
* fix: user uuid is used instead of user id for its uniqueness * fix: use cuid instead & exclude from parser * fix: apply new foreign key constraints to existing data * fix: migration partly done * not-fix: General form of migration achieved, still broken * fix: migrate and use db's uuid function for existing users * fix: Proper not nulling! * fix: #354 * fix: migration & use uuid instead --------- Co-authored-by: dicedtomato <35403473+diced@users.noreply.github.com> Co-authored-by: diced <pranaco2@gmail.com>
This commit is contained in:
parent
eedeb89c7d
commit
5ded128263
8 changed files with 89 additions and 32 deletions
53
prisma/migrations/20230405024416_user_uuid/migration.sql
Normal file
53
prisma/migrations/20230405024416_user_uuid/migration.sql
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[uuid]` on the table `User` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- PRISMA GENERATED BELOW
|
||||||
|
-- -- DropForeignKey
|
||||||
|
-- ALTER TABLE "OAuth" DROP CONSTRAINT "OAuth_userId_fkey";
|
||||||
|
--
|
||||||
|
-- -- AlterTable
|
||||||
|
-- ALTER TABLE "OAuth" ALTER COLUMN "userId" SET DATA TYPE TEXT;
|
||||||
|
--
|
||||||
|
-- -- AlterTable
|
||||||
|
-- ALTER TABLE "User" ADD COLUMN "uuid" UUID NOT NULL DEFAULT gen_random_uuid();
|
||||||
|
--
|
||||||
|
-- -- CreateIndex
|
||||||
|
-- CREATE UNIQUE INDEX "User_uuid_key" ON "User"("uuid");
|
||||||
|
--
|
||||||
|
-- -- AddForeignKey
|
||||||
|
-- ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- User made changes below
|
||||||
|
|
||||||
|
-- Rename old foreign key
|
||||||
|
ALTER TABLE "OAuth" RENAME CONSTRAINT "OAuth_userId_fkey" TO "OAuth_userId_old_fkey";
|
||||||
|
|
||||||
|
-- Rename old column
|
||||||
|
ALTER TABLE "OAuth" RENAME COLUMN "userId" TO "userId_old";
|
||||||
|
|
||||||
|
-- Add new column
|
||||||
|
ALTER TABLE "OAuth" ADD COLUMN "userId" UUID;
|
||||||
|
|
||||||
|
-- Add user uuid
|
||||||
|
ALTER TABLE "User" ADD COLUMN "uuid" UUID NOT NULL DEFAULT gen_random_uuid();
|
||||||
|
|
||||||
|
-- Update table "OAuth" with uuid
|
||||||
|
UPDATE "OAuth" SET "userId" = "User"."uuid" FROM "User" WHERE "OAuth"."userId_old" = "User"."id";
|
||||||
|
|
||||||
|
-- Alter table "OAuth" to make "userId" required
|
||||||
|
ALTER TABLE "OAuth" ALTER COLUMN "userId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- Create index
|
||||||
|
CREATE UNIQUE INDEX "User_uuid_key" ON "User"("uuid");
|
||||||
|
|
||||||
|
-- Add new foreign key
|
||||||
|
ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Drop old foreign key
|
||||||
|
ALTER TABLE "OAuth" DROP CONSTRAINT "OAuth_userId_old_fkey";
|
||||||
|
|
||||||
|
-- Drop old column
|
||||||
|
ALTER TABLE "OAuth" DROP COLUMN "userId_old";
|
|
@ -8,23 +8,24 @@ generator client {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String
|
uuid String @unique @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
password String?
|
username String
|
||||||
avatar String?
|
password String?
|
||||||
token String
|
avatar String?
|
||||||
administrator Boolean @default(false)
|
token String
|
||||||
superAdmin Boolean @default(false)
|
administrator Boolean @default(false)
|
||||||
systemTheme String @default("system")
|
superAdmin Boolean @default(false)
|
||||||
embed Json @default("{}")
|
systemTheme String @default("system")
|
||||||
ratelimit DateTime?
|
embed Json @default("{}")
|
||||||
totpSecret String?
|
ratelimit DateTime?
|
||||||
domains String[]
|
totpSecret String?
|
||||||
oauth OAuth[]
|
domains String[]
|
||||||
files File[]
|
oauth OAuth[]
|
||||||
urls Url[]
|
files File[]
|
||||||
Invite Invite[]
|
urls Url[]
|
||||||
Folder Folder[]
|
Invite Invite[]
|
||||||
|
Folder Folder[]
|
||||||
IncompleteFile IncompleteFile[]
|
IncompleteFile IncompleteFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +113,8 @@ model Invite {
|
||||||
model OAuth {
|
model OAuth {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
provider OauthProviders
|
provider OauthProviders
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [uuid], onDelete: Cascade)
|
||||||
userId Int
|
userId String
|
||||||
username String
|
username String
|
||||||
oauthId String?
|
oauthId String?
|
||||||
token String
|
token String
|
||||||
|
|
|
@ -135,7 +135,7 @@ export const withOAuth =
|
||||||
} else throw e;
|
} else throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setUserCookie(user.id);
|
res.setUserCookie(user.uuid);
|
||||||
logger.info(`User ${user.username} (${user.id}) linked account via oauth(${provider})`);
|
logger.info(`User ${user.username} (${user.id}) linked account via oauth(${provider})`);
|
||||||
|
|
||||||
return res.redirect('/');
|
return res.redirect('/');
|
||||||
|
@ -153,7 +153,7 @@ export const withOAuth =
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.setUserCookie(user.id);
|
res.setUserCookie(user.uuid);
|
||||||
logger.info(`User ${user.username} (${user.id}) logged in via oauth(${provider})`);
|
logger.info(`User ${user.username} (${user.id}) logged in via oauth(${provider})`);
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
|
@ -203,7 +203,7 @@ export const withOAuth =
|
||||||
logger.debug(`created user ${JSON.stringify(nuser)} via oauth(${provider})`);
|
logger.debug(`created user ${JSON.stringify(nuser)} via oauth(${provider})`);
|
||||||
logger.info(`Created user ${nuser.username} via oauth(${provider})`);
|
logger.info(`Created user ${nuser.username} via oauth(${provider})`);
|
||||||
|
|
||||||
res.setUserCookie(nuser.id);
|
res.setUserCookie(nuser.uuid);
|
||||||
logger.info(`User ${nuser.username} (${nuser.id}) logged in via oauth(${provider})`);
|
logger.info(`User ${nuser.username} (${nuser.id}) logged in via oauth(${provider})`);
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
|
|
|
@ -54,7 +54,7 @@ export type NextApiRes = NextApiResponse &
|
||||||
NextApiResExtraObj & {
|
NextApiResExtraObj & {
|
||||||
json: (json: Record<string, unknown>, status?: number) => void;
|
json: (json: Record<string, unknown>, status?: number) => void;
|
||||||
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
|
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
|
||||||
setUserCookie: (id: number) => void;
|
setUserCookie: (id: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ZiplineApiConfig = {
|
export type ZiplineApiConfig = {
|
||||||
|
@ -184,7 +184,7 @@ export const withZipline =
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: Number(userId),
|
uuid: userId,
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
oauth: true,
|
oauth: true,
|
||||||
|
@ -202,22 +202,22 @@ export const withZipline =
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setCookie = (name: string, value: unknown, options: CookieSerializeOptions = {}) => {
|
res.setCookie = (name: string, value: string, options: CookieSerializeOptions = {}) => {
|
||||||
if ('maxAge' in options) {
|
if ('maxAge' in options) {
|
||||||
options.expires = new Date(Date.now() + options.maxAge * 1000);
|
options.expires = new Date(Date.now() + options.maxAge * 1000);
|
||||||
options.maxAge /= 1000;
|
options.maxAge /= 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signed = sign64(String(value), config.core.secret);
|
const signed = sign64(value, config.core.secret);
|
||||||
|
|
||||||
Logger.get('api').debug(`headers(${JSON.stringify(req.headers)}): cookie(${name}, ${value})`);
|
Logger.get('api').debug(`headers(${JSON.stringify(req.headers)}): cookie(${name}, ${value})`);
|
||||||
|
|
||||||
res.setHeader('Set-Cookie', serialize(name, signed, options));
|
res.setHeader('Set-Cookie', serialize(name, signed, options));
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setUserCookie = (id: number) => {
|
res.setUserCookie = (id: string) => {
|
||||||
req.cleanCookie('user');
|
req.cleanCookie('user');
|
||||||
res.setCookie('user', String(id), {
|
res.setCookie('user', id, {
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
expires: new Date(Date.now() + 6.048e8 * 2),
|
expires: new Date(Date.now() + 6.048e8 * 2),
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function parseString(str: string, value: ParseValue) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['password', 'avatar'].includes(matches.groups.prop)) {
|
if (['password', 'avatar', 'uuid'].includes(matches.groups.prop)) {
|
||||||
str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
|
str = replaceCharsFromString(str, '{unknown_property}', matches.index, re.lastIndex);
|
||||||
re.lastIndex = matches.index;
|
re.lastIndex = matches.index;
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -56,7 +56,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (!success) return res.badRequest('Invalid code', { totp: true });
|
if (!success) return res.badRequest('Invalid code', { totp: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.setUserCookie(user.id);
|
res.setUserCookie(user.uuid);
|
||||||
logger.info(`User ${user.username} (${user.id}) logged in`);
|
logger.info(`User ${user.username} (${user.id}) logged in`);
|
||||||
|
|
||||||
return res.json({ success: true });
|
return res.json({ success: true });
|
||||||
|
|
|
@ -12,7 +12,8 @@ export default function OauthError({ error, provider }) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setRemaining((remaining) => remaining - 1);
|
if (remaining > 0) setRemaining((remaining) => remaining - 1);
|
||||||
|
else clearInterval(interval);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
|
@ -43,7 +44,7 @@ export default function OauthError({ error, provider }) {
|
||||||
</Title>
|
</Title>
|
||||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>{error}</MutedText>
|
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>{error}</MutedText>
|
||||||
<MutedText>
|
<MutedText>
|
||||||
Redirecting to login in {remaining} second{remaining === 1 ? 's' : ''}
|
Redirecting to login in {remaining} second{remaining !== 1 ? 's' : ''}
|
||||||
</MutedText>
|
</MutedText>
|
||||||
<Button component={Link} href='/dashboard'>
|
<Button component={Link} href='/dashboard'>
|
||||||
Head to the Dashboard
|
Head to the Dashboard
|
||||||
|
|
|
@ -7,6 +7,8 @@ function postUrlDecorator(fastify: FastifyInstance, _, done) {
|
||||||
done();
|
done();
|
||||||
|
|
||||||
async function postUrl(this: FastifyReply, url: Url) {
|
async function postUrl(this: FastifyReply, url: Url) {
|
||||||
|
if (!url) return true;
|
||||||
|
|
||||||
const nUrl = await this.server.prisma.url.update({
|
const nUrl = await this.server.prisma.url.update({
|
||||||
where: {
|
where: {
|
||||||
id: url.id,
|
id: url.id,
|
||||||
|
|
Loading…
Reference in a new issue