From 70818763a7fa3be24d613265b0fe0fc76568a6f8 Mon Sep 17 00:00:00 2001
From: Radek <104318242+radekm2000@users.noreply.github.com>
Date: Sat, 4 May 2024 12:45:02 +0200
Subject: [PATCH] add signing in with discord
---
.../src/components/DiscordAvatar.tsx | 17 ++++
.../ecommerce/src/components/DiscordIcon.tsx | 9 ++
.../src/components/DiscordSignInButton.tsx | 30 +++++++
.../ClearNotificationsButton.tsx | 28 +++++++
.../NavbarNotifications/NotificationCard.tsx | 39 +++++++++
.../NavbarNotifications/NotificationsList.tsx | 41 +++++++++
.../src/components/PopoverPopupState.tsx | 79 ++---------------
.../ecommerce/src/components/ProfileInfo.tsx | 14 +++-
.../ecommerce/src/components/pages/Inbox.tsx | 6 --
.../ecommerce/src/components/pages/Login.tsx | 4 +-
client/ecommerce/src/types/types.ts | 4 +-
server/ecommerce/package-lock.json | 58 +++++++------
server/ecommerce/package.json | 5 +-
server/ecommerce/src/auth/auth.controller.ts | 23 +++++
server/ecommerce/src/auth/auth.module.ts | 3 +-
server/ecommerce/src/auth/auth.service.ts | 15 +++-
.../ecommerce/src/auth/utils/DiscordGuard.ts | 5 ++
.../src/auth/utils/DiscordStrategy.ts | 84 +++++++++++++++++++
server/ecommerce/src/users/users.service.ts | 19 +++++
.../ecommerce/src/utils/dtos/discord.dto.ts | 14 ++++
.../src/utils/entities/user.entity.ts | 8 +-
21 files changed, 391 insertions(+), 114 deletions(-)
create mode 100644 client/ecommerce/src/components/DiscordAvatar.tsx
create mode 100644 client/ecommerce/src/components/DiscordIcon.tsx
create mode 100644 client/ecommerce/src/components/DiscordSignInButton.tsx
create mode 100644 client/ecommerce/src/components/NavbarNotifications/ClearNotificationsButton.tsx
create mode 100644 client/ecommerce/src/components/NavbarNotifications/NotificationCard.tsx
create mode 100644 client/ecommerce/src/components/NavbarNotifications/NotificationsList.tsx
create mode 100644 server/ecommerce/src/auth/utils/DiscordGuard.ts
create mode 100644 server/ecommerce/src/auth/utils/DiscordStrategy.ts
create mode 100644 server/ecommerce/src/utils/dtos/discord.dto.ts
diff --git a/client/ecommerce/src/components/DiscordAvatar.tsx b/client/ecommerce/src/components/DiscordAvatar.tsx
new file mode 100644
index 0000000..e260e93
--- /dev/null
+++ b/client/ecommerce/src/components/DiscordAvatar.tsx
@@ -0,0 +1,17 @@
+import { Avatar } from "@mui/material";
+
+type Props = {
+ userId: string | undefined;
+ avatar: string;
+};
+
+export const DiscordAvatar = ({ userId, avatar }: Props) => {
+ const avatarUrl = `https://cdn.discordapp.com/avatars/${userId}/${avatar}.png`;
+ console.log(avatarUrl);
+ return (
+
+ );
+};
diff --git a/client/ecommerce/src/components/DiscordIcon.tsx b/client/ecommerce/src/components/DiscordIcon.tsx
new file mode 100644
index 0000000..f9c8f9a
--- /dev/null
+++ b/client/ecommerce/src/components/DiscordIcon.tsx
@@ -0,0 +1,9 @@
+import { SvgIcon, SvgIconProps } from "@mui/material";
+
+export const DiscordIcon = (props: SvgIconProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/client/ecommerce/src/components/DiscordSignInButton.tsx b/client/ecommerce/src/components/DiscordSignInButton.tsx
new file mode 100644
index 0000000..b7c66fa
--- /dev/null
+++ b/client/ecommerce/src/components/DiscordSignInButton.tsx
@@ -0,0 +1,30 @@
+import { Box, BoxProps, Link, Typography } from "@mui/material";
+import { DiscordIcon } from "./DiscordIcon";
+
+export const DiscordSignInButton = (props: BoxProps) => {
+ return (
+
+
+
+
+ SIGN IN
+
+
+
+ );
+};
diff --git a/client/ecommerce/src/components/NavbarNotifications/ClearNotificationsButton.tsx b/client/ecommerce/src/components/NavbarNotifications/ClearNotificationsButton.tsx
new file mode 100644
index 0000000..e06dd87
--- /dev/null
+++ b/client/ecommerce/src/components/NavbarNotifications/ClearNotificationsButton.tsx
@@ -0,0 +1,28 @@
+import { Button } from '@mui/material';
+
+type Props = {
+ onClick(): void
+}
+
+export const ClearNotificationsButton = ({onClick}: Props) => {
+ return (
+
+ )
+}
diff --git a/client/ecommerce/src/components/NavbarNotifications/NotificationCard.tsx b/client/ecommerce/src/components/NavbarNotifications/NotificationCard.tsx
new file mode 100644
index 0000000..3445b45
--- /dev/null
+++ b/client/ecommerce/src/components/NavbarNotifications/NotificationCard.tsx
@@ -0,0 +1,39 @@
+import { Box, CardContent, CardMedia, Typography } from "@mui/material";
+import { ProductNotification } from "../../types/types";
+import { Link } from "wouter";
+
+type Props = {
+ notification: ProductNotification;
+};
+
+export const NotificationCard = ({ notification }: Props) => {
+ return (
+
+
+
+
+
+ {notification.message}
+
+
+ );
+};
diff --git a/client/ecommerce/src/components/NavbarNotifications/NotificationsList.tsx b/client/ecommerce/src/components/NavbarNotifications/NotificationsList.tsx
new file mode 100644
index 0000000..8070024
--- /dev/null
+++ b/client/ecommerce/src/components/NavbarNotifications/NotificationsList.tsx
@@ -0,0 +1,41 @@
+import { Box, CardContent, Typography } from "@mui/material";
+import { ProductNotification } from "../../types/types";
+import NotificationsActiveOutlinedIcon from "@mui/icons-material/NotificationsActiveOutlined";
+import { NotificationCard } from "./NotificationCard";
+
+type Props = {
+ productNotifications: ProductNotification[];
+};
+
+export const NotificationsList = ({ productNotifications }: Props) => {
+ if (productNotifications.length === 0) {
+ return (
+
+
+
+ No notifications yet
+
+ This is where you'll find your notifications
+
+
+
+ );
+ }
+
+ return (
+ <>
+ {productNotifications.map((notification, index) => (
+
+ ))}
+ >
+ );
+};
diff --git a/client/ecommerce/src/components/PopoverPopupState.tsx b/client/ecommerce/src/components/PopoverPopupState.tsx
index 6e754a8..97b7c00 100644
--- a/client/ecommerce/src/components/PopoverPopupState.tsx
+++ b/client/ecommerce/src/components/PopoverPopupState.tsx
@@ -2,19 +2,14 @@ import PopupState, { bindPopover, bindTrigger } from "material-ui-popup-state";
import { ProductNotification } from "../types/types";
import {
Badge,
- Box,
- Button,
Card,
- CardContent,
- CardMedia,
IconButton,
Popover,
- Typography,
} from "@mui/material";
-import NotificationsActiveOutlinedIcon from "@mui/icons-material/NotificationsActiveOutlined";
import NotificationsIcon from "@mui/icons-material/Notifications";
-import { Link } from "wouter";
import { useDeleteProductNotificationsMutation } from "../hooks/useDeleteProductNotificationsMutation";
+import { NotificationsList } from "./NavbarNotifications/NotificationsList";
+import { ClearNotificationsButton } from "./NavbarNotifications/ClearNotificationsButton";
export default function PopoverPopupState({
productNotifications,
@@ -64,77 +59,15 @@ export default function PopoverPopupState({
- {productNotifications.length >= 1 ? (
- productNotifications?.map((notification, index) => (
-
-
-
-
-
- {notification.message}
-
-
- ))
- ) : (
-
-
-
- No notifications yet
-
- This is where you'll find your notifications
-
-
-
- )}
+
+
{productNotifications.length >= 1 && (
-
+ />
)}
diff --git a/client/ecommerce/src/components/ProfileInfo.tsx b/client/ecommerce/src/components/ProfileInfo.tsx
index a55b1c5..b51482c 100644
--- a/client/ecommerce/src/components/ProfileInfo.tsx
+++ b/client/ecommerce/src/components/ProfileInfo.tsx
@@ -15,6 +15,7 @@ import { useEffect, useState } from "react";
import { useFollowUser } from "../utils/followUser";
import { Link, useLocation } from "wouter";
import { calculateMedian } from "../utils/calculateMedian";
+import { DiscordAvatar } from "./DiscordAvatar";
export const ProfileInfo = ({
user,
}: {
@@ -25,6 +26,9 @@ export const ProfileInfo = ({
const ratings = user.reviews.map((review) => review.rating);
const calculatedRatingValue = calculateMedian(ratings);
+ const avatarUrl = `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatar}.png`;
+ console.log(avatarUrl);
+
const isFollowed = (member: UserWithFollows) => {
return (member.followings ?? []).some((following) => {
return following.follower.id === meUser.id;
@@ -106,7 +110,12 @@ export const ProfileInfo = ({
) : (
<>
- {user.avatar ? (
+ {user.role === "discordUser" && user.avatar && (
+
+ )}
+
+
+ {user.role !== "discordUser" && user.avatar && (
- ) : (
+ )}
+ {!(user.role === "discordUser" && user.avatar) && (
{
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -165,10 +166,11 @@ export const Login = () => {
or
-
+
+
diff --git a/client/ecommerce/src/types/types.ts b/client/ecommerce/src/types/types.ts
index 8bf80e9..e179060 100644
--- a/client/ecommerce/src/types/types.ts
+++ b/client/ecommerce/src/types/types.ts
@@ -29,7 +29,8 @@ export type User = {
username: string;
googleId?: string;
email: string;
- role: "admin" | "user";
+ discordId?: string;
+ role: "admin" | "user" | "discordUser";
avatar?: string;
products?: Product[];
};
@@ -122,7 +123,6 @@ export type Brand =
| "Calvin Klein"
| "";
-
export type Message = {
id: number;
content: string;
diff --git a/server/ecommerce/package-lock.json b/server/ecommerce/package-lock.json
index 52b95b8..c7a1d40 100644
--- a/server/ecommerce/package-lock.json
+++ b/server/ecommerce/package-lock.json
@@ -18,6 +18,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.1",
"@types/jsonwebtoken": "^9.0.5",
+ "@types/passport-oauth2": "^1.4.15",
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
@@ -25,7 +26,9 @@
"madge": "^6.1.0",
"nodemailer": "^6.9.9",
"passport": "^0.7.0",
+ "passport-discord": "^0.1.4",
"passport-google-oauth20": "^2.0.0",
+ "passport-oauth2": "^1.8.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
@@ -45,6 +48,7 @@
"@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.14",
+ "@types/passport-discord": "^0.1.13",
"@types/passport-google-oauth20": "^2.0.14",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
@@ -3492,7 +3496,6 @@
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
- "dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
@@ -3502,7 +3505,6 @@
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
- "dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -3552,7 +3554,6 @@
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
- "dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@@ -3564,7 +3565,6 @@
"version": "4.17.41",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz",
"integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==",
- "dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
@@ -3584,8 +3584,7 @@
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
- "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
- "dev": true
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
@@ -3649,8 +3648,7 @@
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
- "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
- "dev": true
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
},
"node_modules/@types/multer": {
"version": "1.4.11",
@@ -3682,7 +3680,6 @@
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.4.tgz",
"integrity": "sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==",
- "dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -3691,11 +3688,21 @@
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.16.tgz",
"integrity": "sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==",
- "dev": true,
"dependencies": {
"@types/express": "*"
}
},
+ "node_modules/@types/passport-discord": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/@types/passport-discord/-/passport-discord-0.1.13.tgz",
+ "integrity": "sha512-A9fGTf69C19xQr4BQUHvK+/DpGuSLF/v/MEMdw4Z46sQd0dqeP+4ysBZqF5rXxaCWVcJKBxXUpV8MpW5jwWVIQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/express": "*",
+ "@types/passport": "*",
+ "@types/passport-oauth2": "*"
+ }
+ },
"node_modules/@types/passport-google-oauth20": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz",
@@ -3711,7 +3718,6 @@
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.15.tgz",
"integrity": "sha512-9cUTP/HStNSZmhxXGuRrBJfEWzIEJRub2eyJu3CvkA+8HAMc9W3aKdFhVq+Qz1hi42qn+GvSAnz3zwacDSYWpw==",
- "dev": true,
"dependencies": {
"@types/express": "*",
"@types/oauth": "*",
@@ -3721,14 +3727,12 @@
"node_modules/@types/qs": {
"version": "6.9.11",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz",
- "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==",
- "dev": true
+ "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ=="
},
"node_modules/@types/range-parser": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
- "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
- "dev": true
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
"node_modules/@types/semver": {
"version": "7.5.6",
@@ -3740,7 +3744,6 @@
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
- "dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
@@ -3750,7 +3753,6 @@
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
- "dev": true,
"dependencies": {
"@types/http-errors": "*",
"@types/mime": "*",
@@ -9403,9 +9405,9 @@
}
},
"node_modules/oauth": {
- "version": "0.9.15",
- "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
- "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
+ "integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
},
"node_modules/object-assign": {
"version": "4.1.1",
@@ -9663,6 +9665,14 @@
"url": "https://github.com/sponsors/jaredhanson"
}
},
+ "node_modules/passport-discord": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz",
+ "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==",
+ "dependencies": {
+ "passport-oauth2": "^1.5.0"
+ }
+ },
"node_modules/passport-google-oauth20": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz",
@@ -9675,12 +9685,12 @@
}
},
"node_modules/passport-oauth2": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
- "integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
+ "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"dependencies": {
"base64url": "3.x.x",
- "oauth": "0.9.x",
+ "oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
diff --git a/server/ecommerce/package.json b/server/ecommerce/package.json
index 56b2372..184369e 100644
--- a/server/ecommerce/package.json
+++ b/server/ecommerce/package.json
@@ -23,7 +23,6 @@
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert",
"typeorm:drop": "npm run typeorm schema:drop",
-
"typeorm:reset": "npm run typeorm:drop && npm run typeorm:sync"
},
"dependencies": {
@@ -36,6 +35,7 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/typeorm": "^10.0.1",
"@types/jsonwebtoken": "^9.0.5",
+ "@types/passport-oauth2": "^1.4.15",
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
@@ -43,7 +43,9 @@
"madge": "^6.1.0",
"nodemailer": "^6.9.9",
"passport": "^0.7.0",
+ "passport-discord": "^0.1.4",
"passport-google-oauth20": "^2.0.0",
+ "passport-oauth2": "^1.8.0",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
@@ -63,6 +65,7 @@
"@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.14",
+ "@types/passport-discord": "^0.1.13",
"@types/passport-google-oauth20": "^2.0.14",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
diff --git a/server/ecommerce/src/auth/auth.controller.ts b/server/ecommerce/src/auth/auth.controller.ts
index 3aa2a5e..adb6193 100644
--- a/server/ecommerce/src/auth/auth.controller.ts
+++ b/server/ecommerce/src/auth/auth.controller.ts
@@ -21,6 +21,8 @@ import { GoogleAuthGuard } from './utils/GoogleGuard';
import { User } from 'src/utils/entities/user.entity';
import { AuthGuard } from './auth.guard';
import { AuthUser } from 'src/decorators/user.decorator';
+import { DiscordAuthGuard } from './utils/DiscordGuard';
+import { DiscordProfile } from 'src/utils/dtos/discord.dto';
@Controller('auth')
export class AuthController {
@@ -46,6 +48,12 @@ export class AuthController {
return await this.authService.handleRefreshToken(request);
}
+ @UseGuards(DiscordAuthGuard)
+ @Get('discord/login')
+ handleDiscordLogin() {
+ //empty method to initialize oauth flow
+ }
+
@UseGuards(GoogleAuthGuard)
@Get('google/login')
handleGoogleLogin() {
@@ -70,4 +78,19 @@ export class AuthController {
});
response.redirect('https://exquisite-pasca-338883.netlify.app');
}
+
+ @UseGuards(DiscordAuthGuard)
+ @Get('discord/redirect')
+ async disordAuthRedirect(@Req() request: Request, @Res() response: Response) {
+ const user = request.user as DiscordProfile;
+ console.log(user);
+ const refreshToken = await this.authService.generateRefreshTokenFor(
+ Number(user.id),
+ );
+ response.cookie('refreshToken', refreshToken, {
+ httpOnly: true,
+ maxAge: 60 * 60 * 1000,
+ });
+ response.redirect('http://localhost:5173');
+ }
}
diff --git a/server/ecommerce/src/auth/auth.module.ts b/server/ecommerce/src/auth/auth.module.ts
index 4bcdc31..557d1df 100644
--- a/server/ecommerce/src/auth/auth.module.ts
+++ b/server/ecommerce/src/auth/auth.module.ts
@@ -11,6 +11,7 @@ import { GoogleStrategy } from './utils/GoogleStrategy';
import { Follow } from 'src/utils/entities/followers.entity';
import { Profile } from 'src/utils/entities/profile.entity';
import { Avatar } from 'src/utils/entities/avatar.entity';
+import { DiscordStrategy } from './utils/DiscordStrategy';
@Module({
imports: [
@@ -21,7 +22,7 @@ import { Avatar } from 'src/utils/entities/avatar.entity';
secret: jwtConstants.secret,
}),
],
- providers: [AuthService, UsersService, GoogleStrategy],
+ providers: [AuthService, UsersService, GoogleStrategy, DiscordStrategy],
controllers: [AuthController],
})
export class AuthModule {}
diff --git a/server/ecommerce/src/auth/auth.service.ts b/server/ecommerce/src/auth/auth.service.ts
index 552d9f4..663e2e3 100644
--- a/server/ecommerce/src/auth/auth.service.ts
+++ b/server/ecommerce/src/auth/auth.service.ts
@@ -9,6 +9,7 @@ import 'dotenv/config';
import { JwtService } from '@nestjs/jwt';
import { jwtConstants } from './constants';
import { Profile } from 'passport-google-oauth20';
+import { DiscordProfile } from 'src/utils/dtos/discord.dto';
@Injectable()
export class AuthService {
constructor(
@@ -45,7 +46,6 @@ export class AuthService {
}
async handleRefreshToken(req: Request) {
- console.log(req.cookies);
const { refreshToken } = req.cookies;
if (!refreshToken) {
@@ -74,6 +74,19 @@ export class AuthService {
return user;
}
+ async createOrGetDiscordUser(
+ profile: DiscordProfile,
+ accessToken: string,
+ refreshToken: string,
+ ) {
+ const user = await this.usersService.findDiscordUserOrCreate(
+ profile.username,
+ profile.avatar,
+ profile.id,
+ );
+ return user;
+ }
+
async generateRefreshTokenFor(userId: number) {
const refreshToken = await this.jwtService.signAsync(
{ sub: userId },
diff --git a/server/ecommerce/src/auth/utils/DiscordGuard.ts b/server/ecommerce/src/auth/utils/DiscordGuard.ts
new file mode 100644
index 0000000..3e6b0f1
--- /dev/null
+++ b/server/ecommerce/src/auth/utils/DiscordGuard.ts
@@ -0,0 +1,5 @@
+import { ExecutionContext, Injectable } from '@nestjs/common';
+import { AuthGuard } from '@nestjs/passport';
+
+@Injectable()
+export class DiscordAuthGuard extends AuthGuard('discord') {}
diff --git a/server/ecommerce/src/auth/utils/DiscordStrategy.ts b/server/ecommerce/src/auth/utils/DiscordStrategy.ts
new file mode 100644
index 0000000..c04c0e1
--- /dev/null
+++ b/server/ecommerce/src/auth/utils/DiscordStrategy.ts
@@ -0,0 +1,84 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { PassportStrategy } from '@nestjs/passport';
+import { Profile, Strategy } from 'passport-discord';
+
+import OAuth2Strategy, {
+ InternalOAuthError,
+ StrategyOptions,
+} from 'passport-oauth2';
+import 'dotenv/config';
+import { AuthService } from '../auth.service';
+import {
+ DiscordProfile,
+ DiscordProfileSchema,
+} from 'src/utils/dtos/discord.dto';
+
+type VerifyCallback = (err?: Error | null, user?: Express.User) => void;
+type UserProfileCallback = (err?: Error | null, profile?: any) => void;
+
+const AUTHORIZATION_URL = 'https://discord.com/oauth2/authorize';
+const TOKEN_URL = 'https://discord.com/api/oauth2/token';
+const PROFILE_URL = 'https://discord.com/api/users/@me';
+@Injectable()
+export class DiscordStrategy extends PassportStrategy(Strategy, 'discord') {
+ private readonly logger: Logger;
+
+ constructor(private readonly authService: AuthService) {
+ super({
+ authorizationURL: AUTHORIZATION_URL,
+ tokenURL: TOKEN_URL,
+ clientID: process.env.DISCORD_CLIENT_ID ?? '',
+ clientSecret: process.env.DISCORD_CLIENT_SECRET ?? '',
+ callbackURL: process.env.DISCORD_REDIRECT_URL ?? '',
+ scope: ['identify', 'guilds'],
+ } as StrategyOptions);
+ this.logger = new Logger(DiscordStrategy.name);
+ }
+
+ public validate = async (
+ accessToken: string,
+ refreshToken: string,
+ profile: DiscordProfile,
+ done: VerifyCallback,
+ ) => {
+ try {
+ console.log(profile);
+ const user = await this.authService.createOrGetDiscordUser(
+ profile,
+ accessToken,
+ refreshToken,
+ );
+ return done(null, user as unknown as Express.User);
+ } catch (error) {
+ return done(error);
+ }
+ };
+
+ public userProfile = async (
+ accessToken: string,
+ done: UserProfileCallback,
+ ) => {
+ this._oauth2.get(PROFILE_URL, accessToken, (err, body) => {
+ if (err || !body) {
+ this.logger.error({ err }, this.profileErrorMessage);
+ return done(new InternalOAuthError(this.profileErrorMessage, err));
+ }
+
+ try {
+ const parsedBody = JSON.parse((body ?? '').toString());
+ return done(null, DiscordProfileSchema.parse(parsedBody));
+ } catch (e) {
+ this.logger.error(
+ { err: e, rawProfile: body },
+ this.profileErrorMessage,
+ );
+ return done(new Error(this.profileErrorMessage));
+ }
+ });
+ };
+
+ private get profileErrorMessage() {
+ return 'Failed to parse user discord profile';
+ }
+ F;
+}
diff --git a/server/ecommerce/src/users/users.service.ts b/server/ecommerce/src/users/users.service.ts
index ba87fe5..c0cb049 100644
--- a/server/ecommerce/src/users/users.service.ts
+++ b/server/ecommerce/src/users/users.service.ts
@@ -375,4 +375,23 @@ export class UsersService {
}
return filteredUsersWithoutMe;
}
+
+ async findDiscordUserOrCreate(username: string, avatar: string, id: string) {
+ const user = await this.usersRepository.findOne({
+ where: { username: username },
+ });
+
+ if (!user) {
+ const newUser = this.usersRepository.create({
+ avatar: avatar,
+ username: username,
+ discordId: id,
+ role: 'discordUser',
+ });
+ await this.usersRepository.save(newUser);
+ return newUser;
+ }
+
+ return user;
+ }
}
diff --git a/server/ecommerce/src/utils/dtos/discord.dto.ts b/server/ecommerce/src/utils/dtos/discord.dto.ts
new file mode 100644
index 0000000..d78969b
--- /dev/null
+++ b/server/ecommerce/src/utils/dtos/discord.dto.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod';
+
+export const DiscordProfileSchema = z.object({
+ id: z.string(),
+ username: z.string(),
+ avatar: z.string().nullish(),
+});
+
+export type DiscordProfile = z.infer;
+
+export type DiscordTokens = {
+ accessToken: string;
+ refreshToken: string;
+};
diff --git a/server/ecommerce/src/utils/entities/user.entity.ts b/server/ecommerce/src/utils/entities/user.entity.ts
index f9c1369..2ab0131 100644
--- a/server/ecommerce/src/utils/entities/user.entity.ts
+++ b/server/ecommerce/src/utils/entities/user.entity.ts
@@ -25,18 +25,21 @@ export class User {
@Column({ nullable: true })
googleId: string;
- @Column()
+ @Column({ nullable: true })
email: string;
@Column({ nullable: true })
password: string;
+ @Column({ nullable: true })
+ discordId: string;
+
@OneToOne(() => Profile, (profile) => profile.user, { cascade: true })
@JoinColumn()
profile: Profile;
@Column({ default: 'user' })
- role: 'admin' | 'user';
+ role: 'admin' | 'user' | 'discordUser';
@Column({ nullable: true })
avatar: string;
@@ -60,7 +63,6 @@ export class User {
@OneToMany(() => Review, (review) => review.reviewRecipient)
reviews: Review[];
-
@BeforeInsert()
async hashPassword() {
if (this.password) {