Skip to content

Commit

Permalink
feat(settings): add profile (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
koenoe authored Jan 10, 2025
1 parent 8c34585 commit 39c724b
Show file tree
Hide file tree
Showing 18 changed files with 675 additions and 54 deletions.
2 changes: 1 addition & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const nextConfig = {
},
{
source: '/settings',
destination: '/settings/import',
destination: '/settings/profile',
permanent: false,
},
];
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"react": "19.0.0",
"react-dom": "19.0.0",
"react-dropzone": "^14.3.5",
"react-fast-compare": "^3.2.2",
"recharts": "^2.15.0",
"slugify": "^1.6.6",
"sonner": "^1.7.1",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 15 additions & 9 deletions src/app/(default)/settings/[tab]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { Suspense } from 'react';
import { notFound, unauthorized } from 'next/navigation';

import ImportContainer from '@/components/Import/ImportContainer';
import ProfileForm from '@/components/Profile/Form';
import { tabs, type Tab } from '@/components/Tabs/Tabs';
import WebhookForPlex from '@/components/Webhook/WebhookForPlex';
import WebhookForPlex, {
plexStyles,
} from '@/components/Webhook/WebhookForPlex';
import auth from '@/lib/auth';

export default async function SettingsPage({
Expand All @@ -26,13 +29,9 @@ export default async function SettingsPage({
return notFound();
}

// if (tab === 'profile') {
// return (
// <p className="text-sm italic">
// Soon you&apos;ll be able to edit your profile details here.
// </p>
// );
// }
if (tab === 'profile') {
return <ProfileForm user={user} />;
}

if (tab === 'import') {
return (
Expand All @@ -59,7 +58,14 @@ export default async function SettingsPage({
<h2 className="text-md mb-4 lg:text-lg">Plex</h2>
<Suspense
fallback={
<div className="relative flex h-28 w-full animate-pulse rounded-lg bg-white/10 shadow-lg" />
<div
className={plexStyles({
className:
'flex h-28 w-full items-center justify-center rounded-lg px-8 shadow-lg',
})}
>
<div className="h-12 w-full animate-pulse rounded-lg bg-black/20" />
</div>
}
>
<WebhookForPlex userId={user.id} />
Expand Down
108 changes: 106 additions & 2 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
'use server';

import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { redirect, unauthorized } from 'next/navigation';
import isEqual from 'react-fast-compare';
import slugify from 'slugify';

import auth from '@/lib/auth';
import { createOTP, validateOTP } from '@/lib/db/otp';
import {
createSession,
deleteSession,
removeTmdbFromSession,
SESSION_DURATION,
} from '@/lib/db/session';
import { createUser, findUser } from '@/lib/db/user';
import {
createUser,
findUser,
removeTmdbFromUser,
updateUser,
} from '@/lib/db/user';
import { sendEmail } from '@/lib/email';
import {
createRequestToken,
Expand Down Expand Up @@ -134,3 +142,99 @@ export async function logout() {
]);
}
}

export async function updateProfile(_: unknown, formData: FormData) {
const { user } = await auth();

if (!user) {
unauthorized();
}

const currentUser = {
email: user.email ?? '',
name: user.name ?? '',
username: user.username ?? '',
};

const rawFormData = {
email: formData.get('email')?.toString() ?? '',
name: formData.get('name')?.toString() ?? '',
username: formData.get('username')?.toString() ?? '',
};

if (isEqual(currentUser, rawFormData)) {
return {
message: 'No changes detected',
success: false,
};
}

const slugifiedUsername = slugify(rawFormData.username, {
lower: true,
strict: true,
trim: true,
});

if (slugifiedUsername !== rawFormData.username) {
return {
message: 'Username can only contain letters, numbers, and dashes',
success: false,
};
}

try {
await updateUser(user, {
email: rawFormData.email,
name: rawFormData.name,
username: slugifiedUsername,
});
} catch (err) {
const error = err as Error;
return {
message: error.message,
success: false,
};
}

return {
message: 'Profile updated successfully',
success: true,
};
}

export async function removeTmdbAccount() {
const { user, session } = await auth();

if (!user || !session) {
unauthorized();
}

const isTmdbUser =
user.tmdbAccountId && user.tmdbAccountObjectId && user.tmdbUsername;
const isTmdbSession = session.tmdbSessionId && session.tmdbAccessToken;

try {
if (isTmdbUser) {
await removeTmdbFromUser(user);
}

if (isTmdbSession) {
await Promise.all([
removeTmdbFromSession(session),
deleteAccessToken(session.tmdbAccessToken),
deleteSessionId(session.tmdbSessionId),
]);
}

return {
message: 'TMDb removed from your account',
success: true,
};
} catch (err) {
const error = err as Error;
return {
message: error.message,
success: false,
};
}
}
35 changes: 33 additions & 2 deletions src/app/api/auth/callback/tmdb/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { type NextRequest } from 'next/server';

import { createSession, SESSION_DURATION } from '@/lib/db/session';
import { createUser, findUser } from '@/lib/db/user';
import auth from '@/lib/auth';
import {
addTmdbToSession,
createSession,
SESSION_DURATION,
} from '@/lib/db/session';
import { addTmdbToUser, createUser, findUser } from '@/lib/db/user';
import {
createAccessToken,
createSessionId,
Expand Down Expand Up @@ -32,6 +37,32 @@ export async function GET(request: NextRequest) {
const tmdbAccount = await fetchAccountDetails(tmdbSessionId);

let user = await findUser({ tmdbAccountId: tmdbAccount.id });

// Note: check if we are connecting an authenticated user
const { user: currentUser, session: currentSession } = await auth();
if (currentUser && currentSession) {
if (user) {
const [baseUrl, queryString] = redirectUri.split('?');
const searchParams = new URLSearchParams(queryString);
searchParams.append('error', 'tmdbAccountAlreadyLinked');
return redirect(`${baseUrl}?${searchParams.toString()}`);
}

await Promise.all([
addTmdbToSession(currentSession, {
tmdbSessionId,
tmdbAccessToken: accessToken,
}),
addTmdbToUser(currentUser, {
tmdbAccountId: tmdbAccount.id,
tmdbAccountObjectId: accountObjectId,
tmdbUsername: tmdbAccount.username,
}),
]);
cookieStore.delete('requestTokenTmdb');
return redirect(redirectUri);
}

if (!user) {
user = await createUser({
name: tmdbAccount.name,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Buttons/LoginButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function LoginButton() {
<button
disabled={pending}
type="submit"
className="hover:bg-primary-700 relative flex h-11 w-full items-center justify-center space-x-3 rounded-lg bg-white px-5 py-2.5 text-center text-sm font-medium text-neutral-900 outline-none transition-colors"
className="relative flex h-11 w-full items-center justify-center space-x-3 rounded-lg bg-white px-5 py-2.5 text-center text-sm font-medium text-neutral-900 outline-none"
>
{pending ? <LoadingDots className="h-3 text-neutral-900" /> : 'Sign in'}
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const MenuItem = ({
return (
<Link
href={href}
className="relative flex h-full w-full items-center overflow-hidden text-3xl lowercase leading-none text-white md:justify-end md:text-base md:leading-none"
className="relative flex h-full w-full items-center overflow-hidden text-3xl lowercase leading-tight text-white md:justify-end md:text-base md:leading-none"
onClick={onClick}
>
<span className="relative h-full truncate text-ellipsis">{label}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Menu/Username.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default async function Username() {
}

const profileName =
user.tmdbUsername || user.username || user.name || 'anonymous';
user.username || user.tmdbUsername || user.name || 'anonymous';

return (
<div className="relative flex h-[18px] w-auto items-center justify-end overflow-hidden text-base lowercase leading-none text-white">
Expand Down
Loading

0 comments on commit 39c724b

Please sign in to comment.