Skip to content

Commit f6b8143

Browse files
committed
Add code for feature account link/unlink and delete.
1 parent 0ec2b12 commit f6b8143

File tree

10 files changed

+404
-3
lines changed

10 files changed

+404
-3
lines changed

account.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
account {
2+
access_token: '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',
3+
token_type: 'bearer',
4+
scope: 'read:user,user:email',
5+
provider: 'github',
6+
type: 'oauth',
7+
providerAccountId: '43227010'
8+
}
9+
profile {
10+
login: 'patelvivekdev',
11+
id: 43227010,
12+
node_id: 'MDQ6VXNlcjQzMjI3MDEw',
13+
avatar_url: 'https://avatars.githubusercontent.com/u/43227010?v=4',
14+
gravatar_id: '',
15+
url: 'https://api.github.com/users/patelvivekdev',
16+
html_url: 'https://github.com/patelvivekdev',
17+
followers_url: 'https://api.github.com/users/patelvivekdev/followers',
18+
following_url: 'https://api.github.com/users/patelvivekdev/following{/other_user}',
19+
gists_url: 'https://api.github.com/users/patelvivekdev/gists{/gist_id}',
20+
starred_url: 'https://api.github.com/users/patelvivekdev/starred{/owner}{/repo}',
21+
subscriptions_url: 'https://api.github.com/users/patelvivekdev/subscriptions',
22+
organizations_url: 'https://api.github.com/users/patelvivekdev/orgs',
23+
repos_url: 'https://api.github.com/users/patelvivekdev/repos',
24+
events_url: 'https://api.github.com/users/patelvivekdev/events{/privacy}',
25+
received_events_url: 'https://api.github.com/users/patelvivekdev/received_events',
26+
type: 'User',
27+
site_admin: false,
28+
name: 'VIVEK PATEL',
29+
company: null,
30+
blog: 'https://patelvivek.dev',
31+
location: 'Canada',
32+
email: 'me@patelvivek.dev',
33+
hireable: true,
34+
bio: 'Ai/ML Enthusiast || Web Dev || Photography 📷.\r\n',
35+
twitter_username: 'patelvivekdev',
36+
public_repos: 77,
37+
public_gists: 15,
38+
followers: 28,
39+
following: 56,
40+
created_at: '2018-09-13T02:43:20Z',
41+
updated_at: '2024-05-27T11:53:46Z',
42+
private_gists: 1,
43+
total_private_repos: 10,
44+
owned_private_repos: 10,
45+
disk_usage: 350910,
46+
collaborators: 1,
47+
two_factor_authentication: true,
48+
plan: {
49+
name: 'pro',
50+
space: 976562499,
51+
collaborators: 0,
52+
private_repos: 9999
53+
}
54+
}

src/app/layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const rubik = Rubik({
2121
});
2222

2323
export const metadata: Metadata = {
24-
title: 'Create Next App',
25-
description: 'Generated by create next app',
24+
title: 'Drizzle + Turso auth',
25+
description: 'A Next.js + Turso + Drizzle auth boilerplate',
2626
};
2727

2828
export default function RootLayout({

src/auth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DrizzleAdapter } from '@auth/drizzle-adapter';
66
import { db } from '@/db';
77
import { loginUser } from './db/query/User';
88
import bcrypt from 'bcryptjs';
9+
import { encode, decode } from 'next-auth/jwt';
910

1011
class InvalidCredentialsError extends AuthError {
1112
code = 'invalid-credentials';
@@ -51,6 +52,14 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
5152
}),
5253
],
5354
callbacks: {
55+
async signIn({ account, profile }) {
56+
console.log('account', account);
57+
console.log('profile', profile);
58+
if (account?.provider === 'google' || account?.provider === 'facebook') {
59+
// Here you can handle additional logic for linking accounts
60+
}
61+
return true;
62+
},
5463
authorized({ auth, request: { nextUrl } }) {
5564
const isLoggedIn = !!auth?.user;
5665
const paths = ['/profile', '/dashboard'];
@@ -81,6 +90,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
8190
},
8291
},
8392
session: { strategy: 'jwt' },
93+
jwt: { encode, decode },
8494
secret: process.env.AUTH_SECRET,
8595
pages: {
8696
signIn: '/sign-in',
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// // components/DeleteUserButton.tsx
2+
// import React from 'react';
3+
4+
// interface DeleteUserButtonProps {
5+
// userId: string;
6+
// }
7+
8+
// const DeleteUserButton: React.FC<DeleteUserButtonProps> = ({ userId }) => {
9+
// const handleDelete = async () => {
10+
// const response = await fetch('/api/auth/delete-user', {
11+
// method: 'POST',
12+
// headers: {
13+
// 'Content-Type': 'application/json',
14+
// },
15+
// body: JSON.stringify({ userId }),
16+
// });
17+
18+
// if (response.ok) {
19+
// alert('User deleted successfully');
20+
// } else {
21+
// alert('Error deleting user');
22+
// }
23+
// };
24+
25+
// return (
26+
// <button onClick={handleDelete}>
27+
// Delete User
28+
// </button>
29+
// );
30+
// };
31+
32+
// export default DeleteUserButton;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// // components/LinkAccountButton.tsx
2+
// import React from 'react';
3+
4+
// interface LinkAccountButtonProps {
5+
// userId: string;
6+
// accountData: AdapterAccount;
7+
// }
8+
9+
// const LinkAccountButton: React.FC<LinkAccountButtonProps> = ({ userId, accountData }) => {
10+
// const handleLink = async () => {
11+
// const response = await fetch('/api/auth/link-account', {
12+
// method: 'POST',
13+
// headers: {
14+
// 'Content-Type': 'application/json',
15+
// },
16+
// body: JSON.stringify({ userId, accountData }),
17+
// });
18+
19+
// if (response.ok) {
20+
// alert('Account linked successfully');
21+
// } else {
22+
// alert('Error linking account');
23+
// }
24+
// };
25+
26+
// return (
27+
// <button onClick={handleLink}>
28+
// Link Account
29+
// </button>
30+
// );
31+
// };
32+
33+
// export default LinkAccountButton;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// // components/UnlinkAccountButton.tsx
2+
// import React from 'react';
3+
4+
// interface UnlinkAccountButtonProps {
5+
// userId: string;
6+
// provider: string;
7+
// providerAccountId: string;
8+
// }
9+
10+
// const UnlinkAccountButton: React.FC<UnlinkAccountButtonProps> = ({ userId, provider, providerAccountId }) => {
11+
// const handleUnlink = async () => {
12+
// const response = await fetch('/api/auth/unlink-account', {
13+
// method: 'POST',
14+
// headers: {
15+
// 'Content-Type': 'application/json',
16+
// },
17+
// body: JSON.stringify({ userId, provider, providerAccountId }),
18+
// });
19+
20+
// if (response.ok) {
21+
// alert('Account unlinked successfully');
22+
// } else {
23+
// alert('Error unlinking account');
24+
// }
25+
// };
26+
27+
// return (
28+
// <button onClick={handleUnlink}>
29+
// Unlink {provider} Account
30+
// </button>
31+
// );
32+
// };
33+
34+
// export default UnlinkAccountButton;

src/db/query/User.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '..';
2-
import { users, accounts } from '../schema';
2+
import { users, accounts, InsertAccounts } from '../schema';
33
import { eq, or } from 'drizzle-orm';
44
import bcrypt from 'bcryptjs';
55

@@ -118,3 +118,34 @@ export const savePassword = async (
118118
};
119119
}
120120
};
121+
122+
// Function to get user by email
123+
export async function getAdapterUser(email: string) {
124+
const user = await db
125+
.select()
126+
.from(users)
127+
.where(eq(users.email, email))
128+
.limit(1);
129+
130+
return user;
131+
}
132+
133+
// Function to link account to user
134+
export async function linkAccountToUser(
135+
userId: string,
136+
accountData: InsertAccounts,
137+
) {
138+
await db.insert(accounts).values({
139+
userId,
140+
type: accountData.type,
141+
provider: accountData.provider,
142+
providerAccountId: accountData.providerAccountId,
143+
access_token: accountData.access_token,
144+
refresh_token: accountData.refresh_token,
145+
expires_at: accountData.expires_at,
146+
token_type: accountData.token_type,
147+
scope: accountData.scope,
148+
id_token: accountData.id_token,
149+
session_state: accountData.session_state,
150+
});
151+
}

src/db/schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,6 @@ export const verificationTokens = sqliteTable(
7070
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
7171
}),
7272
);
73+
74+
export type InsertAccounts = typeof accounts.$inferInsert;
75+
export type SelectAccounts = typeof accounts.$inferSelect;

src/lib/auth.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// // pages/api/auth/[...nextauth].ts
2+
// import NextAuth from 'next-auth';
3+
// import GoogleProvider from 'next-auth/providers/google';
4+
// import FacebookProvider from 'next-auth/providers/facebook';
5+
// import CredentialsProvider from 'next-auth/providers/credentials';
6+
// import { DrizzleAdapter } from '@auth/drizzle-adapter';
7+
// import { db } from '../../../schema';
8+
// import { getAdapterUser, createAdapterUser, linkAccountToUser, createAccount } from '../../../utils/db';
9+
10+
// export default NextAuth({
11+
// adapter: DrizzleAdapter(db),
12+
// providers: [
13+
// CredentialsProvider({
14+
// name: 'Credentials',
15+
// credentials: {
16+
// username: { label: 'Username', type: 'text' },
17+
// password: { label: 'Password', type: 'password' }
18+
// },
19+
// authorize: async (credentials) => {
20+
// const user = await getAdapterUser(credentials.email as string);
21+
// if (user) {
22+
// return user;
23+
// }
24+
25+
// // Create a new user and their account
26+
// const newUser = await createAdapterUser(credentials);
27+
// await createAccount(newUser.id, credentials);
28+
29+
// return newUser;
30+
// }
31+
// }),
32+
// GoogleProvider({
33+
// clientId: process.env.GOOGLE_CLIENT_ID,
34+
// clientSecret: process.env.GOOGLE_CLIENT_SECRET,
35+
// allowDangerousEmailAccountLinking: true
36+
// }),
37+
// FacebookProvider({
38+
// clientId: process.env.FACEBOOK_CLIENT_ID,
39+
// clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
40+
// allowDangerousEmailAccountLinking: true
41+
// }),
42+
// ],
43+
// callbacks: {
44+
// async signIn({ account, profile, user }) {
45+
// if (account.provider === 'google' || account.provider === 'facebook') {
46+
// const existingUser = await getAdapterUser(profile.email as string);
47+
// if (existingUser) {
48+
// await linkAccountToUser(existingUser.id, account);
49+
// }
50+
// }
51+
// return true;
52+
// },
53+
// },
54+
// });
55+
56+
// -----------------------------------------------------
57+
58+
// app/api/auth/unlink-account/route.ts
59+
// import { NextResponse } from 'next/server';
60+
// import { unlinkAccount } from '../../../../utils/db';
61+
62+
// export async function POST(request: Request) {
63+
// const { userId, provider, providerAccountId } = await request.json();
64+
65+
// if (!userId || !provider || !providerAccountId) {
66+
// return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
67+
// }
68+
69+
// try {
70+
// await unlinkAccount(userId, provider, providerAccountId);
71+
// return NextResponse.json({ message: 'Account unlinked successfully' }, { status: 200 });
72+
// } catch (error) {
73+
// return NextResponse.json({ error: 'Error unlinking account' }, { status: 500 });
74+
// }
75+
// }
76+
77+
// -----------------------------------------------------
78+
79+
// app/api/auth/link-account/route.ts
80+
// import { NextResponse } from 'next/server';
81+
// import { linkAccount } from '../../../../utils/db';
82+
// import type { AdapterAccount } from '@auth/core/adapters';
83+
84+
// export async function POST(request: Request) {
85+
// const { userId, accountData } = await request.json();
86+
87+
// if (!userId || !accountData) {
88+
// return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
89+
// }
90+
91+
// try {
92+
// await linkAccount(userId, accountData as AdapterAccount);
93+
// return NextResponse.json({ message: 'Account linked successfully' }, { status: 200 });
94+
// } catch (error) {
95+
// return NextResponse.json({ error: 'Error linking account' }, { status: 500 });
96+
// }
97+
// }
98+
99+
// -----------------------------------------------------
100+
101+
// app/api/auth/delete-user/route.ts
102+
// import { NextResponse } from 'next/server';
103+
// import { deleteUser } from '../../../../utils/db';
104+
105+
// export async function POST(request: Request) {
106+
// const { userId } = await request.json();
107+
108+
// if (!userId) {
109+
// return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
110+
// }
111+
112+
// try {
113+
// await deleteUser(userId);
114+
// return NextResponse.json({ message: 'User deleted successfully' }, { status: 200 });
115+
// } catch (error) {
116+
// return NextResponse.json({ error: 'Error deleting user' }, { status: 500 });
117+
// }
118+
// }

0 commit comments

Comments
 (0)