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:
Jayvin Hernandez 2023-04-04 20:07:41 -07:00 committed by GitHub
parent eedeb89c7d
commit 5ded128263
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 89 additions and 32 deletions

View 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";

View file

@ -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

View file

@ -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');

View file

@ -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: '/',

View file

@ -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;

View file

@ -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 });

View file

@ -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

View file

@ -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,