Merge pull request #979 from AmruthPillai/feature/turbo

Implement Turbo Workspaces, add ARM64 support, fix Google OAuth etc.
This commit is contained in:
Amruth Pillai
2022-08-22 19:59:43 +02:00
committed by GitHub
48 changed files with 1226 additions and 51332 deletions
+7 -1
View File
@@ -1,13 +1,19 @@
# Build Artifacts
dist
.next
.turbo
# IDEs
.vscode
# Project Metadata
.crowdin.yml
# Documentation
README.md
SECURITY.md
CHANGELOG.md
CODE_OF_CONDUCT.md
# Project Dependencies
node_modules
@@ -18,4 +24,4 @@ Dockerfile
docker-compose.yml
# Android App
/app
/app
+12 -6
View File
@@ -1,7 +1,11 @@
# Turbo Cache (Optional)
TURBO_TEAM=
TURBO_TOKEN=
# Server + Client
TZ=UTC
PUBLIC_URL=http://localhost:3000
PUBLIC_SERVER_URL=http://localhost:3000/api
PUBLIC_URL=http://client:3000
PUBLIC_SERVER_URL=http://server:3100
PUBLIC_GOOGLE_CLIENT_ID=
# Server + Database
@@ -18,10 +22,12 @@ JWT_SECRET=
JWT_EXPIRY_TIME=604800
GOOGLE_CLIENT_SECRET=
GOOGLE_API_KEY=
SENDGRID_API_KEY=
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
SENDGRID_FROM_NAME=
SENDGRID_FROM_EMAIL=
MAIL_FROM_NAME=
MAIL_FROM_EMAIL=
MAIL_HOST=
MAIL_PORT=
MAIL_USERNAME=
MAIL_PASSWORD=
STORAGE_BUCKET=
STORAGE_REGION=
STORAGE_ENDPOINT=
+16 -16
View File
@@ -5,27 +5,27 @@
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
"plugins": ["@typescript-eslint/eslint-plugin", "simple-import-sort", "unused-imports"],
"rules": {
// Unused Imports
"no-unused-vars": "off",
// Simple Import Sort
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"args": "none",
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_"
}
],
"unused-imports/no-unused-imports": "error",
// TypeScript ESLint
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/interface-name-prefix": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
// Simple Import Sort
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
// Unused Imports
"no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "none",
"argsIgnorePattern": "^_"
}
]
"@typescript-eslint/explicit-module-boundary-types": "off"
},
"overrides": [
{
+12 -5
View File
@@ -1,36 +1,43 @@
---
name: Bug Report
about: Create a report to help improve
title: "[BUG] "
title: '[BUG] '
labels: bug
assignees: ''
---
**Describe the bug**
<!-- A clear and concise description of what the bug is. -->
**Product Flavor**
- [ ] Managed (https://rxresu.me)
- [ ] Self Hosted
**To Reproduce**
<!-- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, add screenshots to help explain your problem. -->
**Desktop (please complete the following information):**
- OS: <!--[e.g. iOS]-->
- Browser <!--[e.g. chrome, safari]-->
- Version <!--[e.g. 22]-->
- OS: <!--[e.g. iOS]-->
- Browser <!--[e.g. chrome, safari]-->
- Version <!--[e.g. 22]-->
**Additional context**
<!-- Add any other context about the problem here. -->
+5 -2
View File
@@ -1,20 +1,23 @@
---
name: Feature Request
about: Suggest an idea for this project
title: "[FEATURE] "
title: '[FEATURE] '
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
+12
View File
@@ -9,9 +9,15 @@ jobs:
name: Client
runs-on: ubuntu-latest
env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.0.2
with:
fetch-depth: 2
- id: version
name: Get Version
@@ -46,9 +52,15 @@ jobs:
name: Server
runs-on: ubuntu-latest
env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.0.2
with:
fetch-depth: 2
- id: version
name: Get Version
+88
View File
@@ -0,0 +1,88 @@
name: Build Docker Image
on: pull_request
jobs:
client:
name: Client
runs-on: ubuntu-latest
env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.0.2
with:
fetch-depth: 2
- id: vars
name: Get Short SHA
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- id: buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
with:
install: true
- id: version
name: Get Version
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Build Client Image
uses: docker/build-push-action@v3.1.1
with:
context: .
push: false
file: client/Dockerfile
platforms: linux/amd64,linux/arm64
tags: |
amruthpillai/reactive-resume:client-latest
amruthpillai/reactive-resume:client-${{ steps.vars.outputs.sha_short }}
ghcr.io/amruthpillai/reactive-resume:client-latest
ghcr.io/amruthpillai/reactive-resume:client-${{ steps.vars.outputs.sha_short }}
server:
name: Server
runs-on: ubuntu-latest
env:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
steps:
- name: Checkout the repository
uses: actions/checkout@v3.0.2
with:
fetch-depth: 2
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.0.0
- id: buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2.0.0
with:
install: true
- id: version
name: Get Version
run: echo ::set-output name=tag::${GITHUB_REF#refs/*/}
- name: Build Server Image
uses: docker/build-push-action@v3.1.1
with:
context: .
push: false
file: server/Dockerfile
platforms: linux/amd64,linux/arm64
tags: |
amruthpillai/reactive-resume:server-latest
amruthpillai/reactive-resume:server-${{ steps.vars.outputs.sha_short }}
ghcr.io/amruthpillai/reactive-resume:server-latest
ghcr.io/amruthpillai/reactive-resume:server-${{ steps.vars.outputs.sha_short }}
+4 -1
View File
@@ -7,4 +7,7 @@
node_modules
# macOS
.DS_Store
.DS_Store
# Turbo
.turbo
-6
View File
@@ -1,6 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm install
pnpm run lint
pnpm run format
+1
View File
@@ -18,6 +18,7 @@ CHANGELOG.md
# Project Dependencies
node_modules
pnpm-lock.yaml
# Docker
Dockerfile
+6 -2
View File
@@ -1,3 +1,7 @@
{
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "lokalise.i18n-ally"]
}
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"lokalise.i18n-ally"
]
}
+6 -6
View File
@@ -3,12 +3,12 @@
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Debug: Server",
"port": 9229,
"request": "attach",
"restart": true,
"stopOnEntry": false,
"protocol": "inspector"
"protocol": "inspector",
"stopOnEntry": false
},
{
"name": "Debug: Client",
@@ -17,10 +17,10 @@
"command": "pnpm run dev:client",
"console": "integratedTerminal",
"serverReadyAction": {
"action": "debugWithChrome",
"pattern": "started server on .+, url: (https?://.+)",
"uriFormat": "%s",
"action": "debugWithChrome"
"uriFormat": "%s"
}
}
]
}
}
+5 -14
View File
@@ -1,27 +1,18 @@
{
"css.validate": false,
"scss.validate": false,
"editor.wordWrap": "on",
"npm.packageManager": "pnpm",
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.wordWrap": "on",
"eslint.workingDirectories": [
"schema",
"client",
"server"
],
"i18n-ally.enabledFrameworks": [
"react"
],
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": [
"client/public/locales"
],
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/{namespaces}.{ext}",
"i18n-ally.sortKeys": true,
"scss.validate": false,
"conventionalCommits.scopes": [
"client",
"server",
+11 -11
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
+17 -21
View File
@@ -1,22 +1,19 @@
FROM node:lts-alpine as dependencies
RUN apk add --no-cache curl g++ make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
FROM node:lts-alpine AS base
WORKDIR /app
COPY package.json pnpm-*.yaml ./
RUN apk add --no-cache g++ git curl make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
FROM base as dependencies
COPY package.json pnpm-*.yaml turbo.json ./
COPY ./schema/package.json ./schema/package.json
COPY ./client/package.json ./client/package.json
RUN pnpm install --frozen-lockfile
FROM node:lts-alpine as builder
RUN apk add --no-cache curl g++ make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
WORKDIR /app
FROM base as builder
COPY . .
@@ -24,21 +21,20 @@ COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
COPY --from=dependencies /app/client/node_modules ./client/node_modules
RUN pnpm run build:schema
RUN pnpm run build:client
ARG TURBO_TEAM
ARG TURBO_TOKEN
FROM node:lts-alpine as production
ENV TURBO_TEAM $TURBO_TEAM
ENV TURBO_TOKEN $TURBO_TOKEN
WORKDIR /app
RUN pnpm run build --filter client
RUN apk add --no-cache curl \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
FROM base as production
COPY --from=builder /app/pnpm-*.yaml ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/package.json /app/pnpm-*.yaml /app/turbo.json ./
COPY --from=builder /app/client/package.json ./client/package.json
RUN pnpm install -F client --frozen-lockfile --prod
RUN pnpm install --filter client --prod --frozen-lockfile --workspace-root
COPY --from=builder /app/client/.next ./client/.next
COPY --from=builder /app/client/public ./client/public
@@ -52,4 +48,4 @@ ENV PORT 3000
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
CMD curl -fSs 127.0.0.1:3000 || exit 1
CMD [ "pnpm", "run", "start:client" ]
CMD [ "pnpm", "run", "start", "--filter", "client" ]
@@ -97,7 +97,7 @@ const Settings = () => {
<List sx={{ padding: 0 }}>
{/* Global Settings */}
<>
<ListSubheader className="rounded">
<ListSubheader disableSticky className="rounded">
{t<string>('builder.rightSidebar.sections.settings.global.heading')}
</ListSubheader>
@@ -155,7 +155,7 @@ const Settings = () => {
{/* Page Settings */}
<>
<ListSubheader className="rounded">
<ListSubheader disableSticky className="rounded">
{t<string>('builder.rightSidebar.sections.settings.page.heading')}
</ListSubheader>
@@ -187,7 +187,7 @@ const Settings = () => {
{/* Resume Settings */}
<>
<ListSubheader className="rounded">
<ListSubheader disableSticky className="rounded">
{t<string>('builder.rightSidebar.sections.settings.resume.heading')}
</ListSubheader>
+3 -3
View File
@@ -1,6 +1,6 @@
import { createTheme } from '@mui/material';
import { createTheme, ThemeOptions } from '@mui/material/styles';
const theme = createTheme({
const theme: ThemeOptions = {
typography: {
fontSize: 12,
fontFamily: 'Inter, sans-serif',
@@ -49,7 +49,7 @@ const theme = createTheme({
},
},
},
});
};
export const lightTheme = createTheme({
...theme,
+11 -32
View File
@@ -1,7 +1,8 @@
import env from '@beam-australia/react-env';
import { joiResolver } from '@hookform/resolvers/joi';
import { Google, Login, Visibility, VisibilityOff } from '@mui/icons-material';
import { Login, Visibility, VisibilityOff } from '@mui/icons-material';
import { Button, IconButton, InputAdornment, TextField } from '@mui/material';
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
import Joi from 'joi';
import { isEmpty } from 'lodash';
import { Trans, useTranslation } from 'next-i18next';
@@ -17,8 +18,6 @@ import { ServerError } from '@/services/axios';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
declare const google: any;
type FormData = {
identifier: string;
password: string;
@@ -85,28 +84,16 @@ const LoginModal: React.FC = () => {
dispatch(setModalState({ modal: 'auth.forgot', state: { open: true } }));
};
const handleLoginWithGoogle = async () => {
console.log(process.env.PUBLIC_GOOGLE_CLIENT_ID, env('GOOGLE_CLIENT_ID'));
const handleLoginWithGoogle = async (response: CredentialResponse) => {
if (response.credential) {
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
google.accounts.id.initialize({
auto_select: true,
itp_support: true,
client_id: env('GOOGLE_CLIENT_ID'),
callback: async (response: any) => {
await loginWithGoogleMutation({ credential: response.credential });
handleClose();
}
};
handleClose();
},
});
google.accounts.id.prompt((notification: any) => {
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
const reason = notification.getNotDisplayedReason() || notification.getSkippedReason();
toast.error(`Google returned an error while trying to sign in: ${reason}.`);
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
}
});
const handleLoginWithGoogleError = () => {
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
};
const PasswordVisibility = (): React.ReactElement => {
@@ -130,15 +117,7 @@ const LoginModal: React.FC = () => {
footerChildren={
<div className="flex gap-4">
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
<Button
type="submit"
variant="outlined"
disabled={isLoading}
startIcon={<Google />}
onClick={handleLoginWithGoogle}
>
{t<string>('modals.auth.login.actions.google')}
</Button>
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
)}
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
+12 -22
View File
@@ -1,11 +1,13 @@
import env from '@beam-australia/react-env';
import { joiResolver } from '@hookform/resolvers/joi';
import { Google, HowToReg } from '@mui/icons-material';
import { HowToReg } from '@mui/icons-material';
import { Button, TextField } from '@mui/material';
import { CredentialResponse, GoogleLogin } from '@react-oauth/google';
import Joi from 'joi';
import { isEmpty } from 'lodash';
import { Trans, useTranslation } from 'next-i18next';
import { Controller, useForm } from 'react-hook-form';
import toast from 'react-hot-toast';
import { useMutation } from 'react-query';
import BaseModal from '@/components/shared/BaseModal';
@@ -14,8 +16,6 @@ import { ServerError } from '@/services/axios';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { setModalState } from '@/store/modal/modalSlice';
declare const google: any;
type FormData = {
name: string;
username: string;
@@ -79,18 +79,16 @@ const RegisterModal: React.FC = () => {
dispatch(setModalState({ modal: 'auth.login', state: { open: true } }));
};
const handleLoginWithGoogle = async () => {
google.accounts.id.initialize({
client_id: env('GOOGLE_CLIENT_ID'),
callback: async (response: any) => {
await loginWithGoogleMutation({ credential: response.credential });
const handleLoginWithGoogle = async (response: CredentialResponse) => {
if (response.credential) {
await loginWithGoogleMutation({ credential: response.credential }, { onError: handleLoginWithGoogleError });
handleClose();
},
auto_select: false,
});
handleClose();
}
};
google.accounts.id.prompt();
const handleLoginWithGoogleError = () => {
toast("Please try logging in using email/password, or use another browser that supports Google's One Tap API.");
};
return (
@@ -102,15 +100,7 @@ const RegisterModal: React.FC = () => {
footerChildren={
<div className="flex gap-4">
{!isEmpty(env('GOOGLE_CLIENT_ID')) && (
<Button
type="submit"
variant="outlined"
disabled={isLoading}
startIcon={<Google />}
onClick={handleLoginWithGoogle}
>
{t<string>('modals.auth.register.actions.google')}
</Button>
<GoogleLogin onSuccess={handleLoginWithGoogle} onError={handleLoginWithGoogleError} />
)}
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isLoading}>
+27 -23
View File
@@ -2,9 +2,9 @@
"name": "@reactive-resume/client",
"scripts": {
"dev": "react-env --prefix PUBLIC -- next dev",
"lint": "next lint --fix",
"build": "next build && npm run sitemap",
"start": "react-env --prefix PUBLIC -- next start",
"lint": "next lint --fix",
"sitemap": "next-sitemap --config next-sitemap.config.js"
},
"dependencies": {
@@ -16,30 +16,31 @@
"@hookform/resolvers": "2.9.7",
"@monaco-editor/react": "^4.4.5",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.94",
"@mui/material": "^5.10.0",
"@mui/system": "^5.10.0",
"@mui/x-date-pickers": "5.0.0-beta.5",
"@next/env": "^12.2.4",
"@reduxjs/toolkit": "^1.8.4",
"@mui/lab": "^5.0.0-alpha.95",
"@mui/material": "^5.10.1",
"@mui/system": "^5.10.1",
"@mui/x-date-pickers": "5.0.0-beta.6",
"@next/env": "^12.2.5",
"@react-oauth/google": "^0.2.6",
"@reduxjs/toolkit": "^1.8.5",
"axios": "^0.27.2",
"clsx": "^1.2.1",
"dayjs": "^1.11.4",
"dayjs": "^1.11.5",
"downloadjs": "^1.4.7",
"joi": "^17.6.0",
"lodash": "^4.17.21",
"md5-hex": "^4.0.0",
"monaco-editor": "^0.34.0",
"nanoid": "^3.3.4",
"next": "12.2.4",
"next-i18next": "^11.3.0",
"react": "18.2.0",
"next": "12.2.5",
"next-i18next": "^12.0.0",
"react": "18",
"react-beautiful-dnd": "^13.1.0",
"react-colorful": "^5.6.0",
"react-colorful": "^5.6.1",
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.34.0",
"react-dom": "18",
"react-hook-form": "^7.34.2",
"react-hot-toast": "2.3.0",
"react-hotkeys-hook": "^3.4.7",
"react-icons": "^4.4.0",
@@ -49,33 +50,36 @@
"react-zoom-pan-pinch": "^2.1.3",
"redux": "^4.2.0",
"redux-persist": "^6.0.0",
"redux-saga": "^1.1.3",
"redux-saga": "^1.2.1",
"remark-gfm": "^3.0.1",
"sharp": "^0.30.7",
"uuid": "^8.3.2",
"webfontloader": "^1.6.28"
},
"resolutions": {
"@types/react": "18",
"@types/react-dom": "18"
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@reactive-resume/schema": "workspace:*",
"@tailwindcss/typography": "^0.5.4",
"@types/downloadjs": "^1.4.3",
"@types/lodash": "^4.14.182",
"@types/node": "18.7.1",
"@types/react": "18.0.17",
"@types/lodash": "^4.14.184",
"@types/node": "18.7.9",
"@types/react": "18",
"@types/react-beautiful-dnd": "^13.1.2",
"@types/react-dom": "18",
"@types/react-redux": "^7.1.24",
"@types/tailwindcss": "^3.0.11",
"@types/uuid": "^8.3.4",
"@types/webfontloader": "^1.6.34",
"autoprefixer": "^10.4.8",
"csstype": "^3.1.0",
"eslint": "^8.21.0",
"eslint-config-next": "12.2.4",
"next-sitemap": "^3.1.17",
"eslint-config-next": "12.2.5",
"next-sitemap": "^3.1.21",
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"sass": "^1.54.4",
"sass": "^1.54.5",
"tailwindcss": "^3.1.8",
"typescript": "^4.7.4"
}
@@ -3,7 +3,6 @@ import clsx from 'clsx';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { GetServerSideProps, NextPage } from 'next';
import { useRouter } from 'next/router';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useEffect } from 'react';
@@ -55,8 +54,6 @@ export const getServerSideProps: GetServerSideProps<Props | Promise<Props>, Quer
};
const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
const router = useRouter();
const dispatch = useAppDispatch();
const resume = useAppSelector((state) => state.resume);
@@ -65,12 +62,6 @@ const Printer: NextPage<Props> = ({ resume: initialData, locale }) => {
if (initialData) dispatch(setResume(initialData));
}, [dispatch, initialData]);
useEffect(() => {
const { pathname, asPath, query } = router;
router.push({ pathname, query }, asPath, { locale });
}, [router, locale]);
if (!resume || isEmpty(resume)) return null;
const layout: string[][][] = get(resume, 'metadata.layout', []);
+18 -17
View File
@@ -1,10 +1,11 @@
import '@/styles/globals.scss';
import env from '@beam-australia/react-env';
import DayjsAdapter from '@date-io/dayjs';
import { LocalizationProvider } from '@mui/x-date-pickers';
import { GoogleOAuthProvider } from '@react-oauth/google';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import Script from 'next/script';
import { appWithTranslation } from 'next-i18next';
import { Toaster } from 'react-hot-toast';
import { QueryClientProvider } from 'react-query';
@@ -34,27 +35,27 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
<ReduxProvider store={store}>
<LocalizationProvider dateAdapter={DayjsAdapter}>
<PersistGate loading={null} persistor={persistor}>
<QueryClientProvider client={queryClient}>
<WrapperRegistry>
<Loading />
<GoogleOAuthProvider clientId={env('GOOGLE_CLIENT_ID')}>
<QueryClientProvider client={queryClient}>
<WrapperRegistry>
<Loading />
<Component {...pageProps} />
<Component {...pageProps} />
<ModalWrapper />
<Toaster
position="bottom-right"
toastOptions={{
duration: 4000,
className: 'toast',
}}
/>
</WrapperRegistry>
</QueryClientProvider>
<ModalWrapper />
<Toaster
position="bottom-right"
toastOptions={{
duration: 4000,
className: 'toast',
}}
/>
</WrapperRegistry>
</QueryClientProvider>
</GoogleOAuthProvider>
</PersistGate>
</LocalizationProvider>
</ReduxProvider>
<Script src="https://accounts.google.com/gsi/client" async defer />
</>
);
};
+1 -2
View File
@@ -15,8 +15,7 @@
},
"login": {
"actions": {
"login": "Login",
"google": "Login with Google"
"login": "Login"
},
"body": "Please enter your username and password associated with your account to login and access, manage and share your resumes.",
"form": {
+21 -21
View File
@@ -1,39 +1,39 @@
{
"extends": "../tsconfig.base.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": [".next"],
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"jsx": "preserve",
"target": "es5",
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"allowJs": true,
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": "./",
"forceConsistentCasingInFileNames": true,
"paths": {
"@/components/*": ["components/*"],
"@/config/*": ["config/*"],
"@/constants/*": ["constants/*"],
"@/data/*": ["data/*"],
"@/i18n/*": ["i18n/*"],
"@/modals/*": ["modals/*"],
"@/pages/*": ["pages/*"],
"@/public/*": ["public/*"],
"@/services/*": ["services/*"],
"@/store/*": ["store/*"],
"@/styles/*": ["styles/*"],
"@/templates/*": ["templates/*"],
"@/types/*": ["types/*"],
"@/utils/*": ["utils/*"],
"@/wrappers/*": ["wrappers/*"]
"@/config/*": ["config/*"],
"@/modals/*": ["modals/*"],
"@/public/*": ["public/*"],
"@/styles/*": ["styles/*"],
"@/services/*": ["services/*"],
"@/wrappers/*": ["wrappers/*"],
"@/constants/*": ["constants/*"],
"@/templates/*": ["templates/*"],
"@/components/*": ["components/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": [".next"]
}
}
+14 -47
View File
@@ -1,15 +1,12 @@
version: '3'
version: '3.8'
services:
postgres:
image: postgres:14.2-alpine
image: postgres:alpine
container_name: postgres
ports:
- 5432:5432
environment:
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
env_file: .env.docker
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
@@ -21,7 +18,7 @@ services:
restart: always
traefik:
image: traefik:rocamadour
image: traefik:latest
container_name: traefik
command:
- --providers.docker=true
@@ -33,37 +30,12 @@ services:
- /var/run/docker.sock:/var/run/docker.sock:ro
server:
image: amruthpillai/reactive-resume:server-latest
# build:
# context: .
# dockerfile: ./server/Dockerfile
# image: amruthpillai/reactive-resume:server-latest
build:
context: .
dockerfile: ./server/Dockerfile
container_name: server
environment:
- TZ=UTC
- PUBLIC_URL=http://localhost:3000
- PUBLIC_SERVER_URL=http://localhost:3000/api
- PUBLIC_GOOGLE_CLIENT_ID=
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- SECRET_KEY=
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_SSL_CERT=
- JWT_SECRET=
- JWT_EXPIRY_TIME=604800
- GOOGLE_CLIENT_SECRET=
- GOOGLE_API_KEY=
- SENDGRID_API_KEY=
- SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID=
- SENDGRID_FROM_NAME=
- SENDGRID_FROM_EMAIL=
- STORAGE_BUCKET=
- STORAGE_REGION=
- STORAGE_ENDPOINT=
- STORAGE_URL_PREFIX=
- STORAGE_ACCESS_KEY=
- STORAGE_SECRET_KEY=
env_file: .env.docker
depends_on:
- traefik
- postgres
@@ -77,17 +49,12 @@ services:
restart: always
client:
image: amruthpillai/reactive-resume:client-latest
# build:
# context: .
# dockerfile: ./client/Dockerfile
# image: amruthpillai/reactive-resume:client-latest
build:
context: .
dockerfile: ./client/Dockerfile
container_name: client
environment:
- TZ=UTC
- PUBLIC_URL=http://localhost:3000
- PUBLIC_SERVER_URL=http://localhost:3000/api
- PUBLIC_GOOGLE_CLIENT_ID=
- PUBLIC_FLAG_DISABLE_SIGNUPS=false
env_file: .env.docker
depends_on:
- traefik
- server
+1 -1
View File
@@ -2,7 +2,7 @@
/node_modules
# Production
/build
build
# Generated files
.docusaurus
+4 -18
View File
@@ -136,30 +136,16 @@ You can get your own key here: https://developers.google.com/fonts/docs/develope
If you do not have a Google API Key, it was make use of the cached response JSON that's stored within the project source. Please note that this cache is not updated and may not have all the latest fonts that Google Fonts has to offer.
## SendGrid
## Mail
The server makes use of SendGrid to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.**
The server makes use of SMTP to send the password reset email to those who have forgotten their password. **This section is completely optional for those who do not require this functionality.**
### `SENDGRID_API_KEY`
**Required**: `no`
**Description:** SendGrid API Key
Does not require any payment or credit card information to obtain an API key.
You can get your own key here: https://docs.sendgrid.com/ui/account-and-settings/api-keys
### `SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID`
**Required**: `no`
**Description:** Dynamic Template ID for Forgot Password
### `SENDGRID_FROM_NAME`
### `MAIL_FROM_NAME`
**Required**: `no`
**Description:** Sender's Name
### `SENDGRID_FROM_EMAIL`
### `MAIL_FROM_EMAIL`
**Required**: `no`
**Description:** Sender's Email Address
+19 -6
View File
@@ -4,7 +4,6 @@ sidebar_position: 2
# Create a new resume
To create a new resume from scratch, click on the selected part.
<a href="https://rxresu.me/dashboard">
@@ -12,13 +11,27 @@ To create a new resume from scratch, click on the selected part.
</a>
<p>
After you click on it, enter name and select whether you want it publicly accessible
<img align="center" src="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655884469/on-button_a5kfbz.png" height='25' type="image"></img> or not
<img align="center" src="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655884949/off-button_rrkz3g.png" height='25' type="image"></img> . You can also change it later from the sharing <a href="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655889311/toggle_jcmfix.gif">see where</a>.
After you click on it, enter name and select whether you want it publicly accessible
<img
align="center"
src="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655884469/on-button_a5kfbz.png"
height="25"
type="image"
></img> or not
<img
align="center"
src="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655884949/off-button_rrkz3g.png"
height="25"
type="image"
></img> . You can also change it later from the sharing{' '}
<a href="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655889311/toggle_jcmfix.gif">see where</a>.
</p>
<h3> Now it's look like ⚡</h3>
<a href="https://rxresu.me/dashboard">
<img src="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655882355/Screenshot_2022-06-22_at_12.48.42_PM_dx6714.png" type="image"></img>
<img
src="https://res.cloudinary.com/dn2mupqo0/image/upload/v1655882355/Screenshot_2022-06-22_at_12.48.42_PM_dx6714.png"
type="image"
></img>
</a>
-5
View File
@@ -1,10 +1,6 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Reactive Resume',
tagline: 'A free and open source resume builder.',
@@ -24,7 +20,6 @@ const config = {
blog: false,
docs: {
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/AmruthPillai/Reactive-Resume/tree/main/docs',
},
sitemap: {
+9 -17
View File
@@ -3,25 +3,17 @@
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start --no-open --port 3200",
"dev": "docusaurus start --no-open --port 3200",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve --port 3200",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
"start": "docusaurus serve --no-open --port 3200"
},
"dependencies": {
"@algolia/client-search": "^4.14.2",
"@docusaurus/core": "2.0.1",
"@docusaurus/preset-classic": "2.0.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"@mdx-js/react": "^1.6.22",
"@docusaurus/core": "2.0.1",
"prism-react-renderer": "^1.3.5",
"@docusaurus/preset-classic": "2.0.1"
},
"browserslist": {
"production": [
@@ -36,7 +28,7 @@
]
},
"devDependencies": {
"@types/react": "^17.0.2",
"typescript": "^4.7.4"
"typescript": "^4.7.4",
"@types/react": "^17.0.2"
}
}
-31
View File
@@ -1,31 +0,0 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }],
// But you can create a sidebar manually
/*
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial',
items: ['hello'],
},
],
*/
};
module.exports = sidebars;
-49834
View File
File diff suppressed because it is too large Load Diff
+19 -38
View File
@@ -1,55 +1,36 @@
{
"name": "reactive-resume",
"version": "3.5.3",
"version": "3.6.0",
"private": true,
"scripts": {
"dev": "env-cmd --silent turbo run dev",
"lint": "eslint --fix .",
"build": "env-cmd --silent turbo run build",
"start": "env-cmd --silent turbo run start",
"format": "prettier --write .",
"release": "standard-version --release-as patch"
},
"workspaces": [
"schema",
"client",
"server",
"docs"
],
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"scripts": {
"prepare": "husky install",
"release": "standard-version",
"lint": "eslint --quiet --fix --ext .js,.ts,.tsx .",
"format": "prettier --loglevel error --write \"./**/*.{js,ts,tsx,json}\"",
"dev:schema": "pnpm -F schema dev",
"dev:server": "pnpm -F server start:debug",
"dev:client": "pnpm -F client dev",
"dev:docs": "pnpm -F docs start",
"dev": "env-cmd --silent concurrently --kill-others \"pnpm run dev:*\"",
"build:schema": "pnpm -F schema build",
"build:server": "pnpm -F server build",
"build:client": "pnpm -F client build",
"build:docs": "pnpm -F docs build",
"build": "env-cmd --silent concurrently \"pnpm run build:*\"",
"start:server": "pnpm -F server start:prod",
"start:client": "pnpm -F client start",
"start:docs": "pnpm -F docs serve",
"start": "env-cmd --silent concurrently --kill-others \"pnpm run start:*\""
},
"dependencies": {
"concurrently": "^7.3.0",
"turbo": "^1.4.3",
"env-cmd": "^10.1.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^7.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^8.0.1",
"eslint": "^8.22.0",
"prettier": "^2.7.1",
"typescript": "^4.7.4",
"standard-version": "^9.5.0",
"typescript": "^4.7.4"
"eslint-plugin-import": "^2.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"@typescript-eslint/parser": "^5.33.1",
"eslint-plugin-unused-imports": "^2.0.0",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"eslint-plugin-simple-import-sort": "^7.0.0"
}
}
+681 -998
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -4,12 +4,11 @@
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"lint": "eslint --fix --ext .ts ./src"
"build": "tsc"
},
"devDependencies": {
"eslint": "^8.21.0",
"eslint": "^8.22.0",
"typescript": "^4.7.4"
}
}
+22 -19
View File
@@ -1,22 +1,19 @@
FROM node:lts-alpine as dependencies
RUN apk add --no-cache g++ curl make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
FROM node:lts-alpine AS base
WORKDIR /app
COPY package.json pnpm-*.yaml ./
RUN apk add --no-cache g++ git curl make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
FROM base AS dependencies
COPY package.json pnpm-*.yaml turbo.json ./
COPY ./schema/package.json ./schema/package.json
COPY ./server/package.json ./server/package.json
RUN pnpm install --frozen-lockfile
FROM node:lts-alpine as builder
RUN apk add --no-cache g++ curl make python3 \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
WORKDIR /app
FROM base AS builder
COPY . .
@@ -24,25 +21,31 @@ COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/schema/node_modules ./schema/node_modules
COPY --from=dependencies /app/server/node_modules ./server/node_modules
RUN pnpm run build:schema
RUN pnpm run build:server
ARG TURBO_TEAM
ARG TURBO_TOKEN
FROM mcr.microsoft.com/playwright:focal as production
ENV TURBO_TEAM $TURBO_TEAM
ENV TURBO_TOKEN $TURBO_TOKEN
RUN pnpm run build --filter server
FROM mcr.microsoft.com/playwright:next-jammy as production
WORKDIR /app
RUN apt-get update \
&& apt-get install -y curl \
&& apt-get install -y curl build-essential \
&& curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm
COPY --from=builder /app/pnpm-*.yaml ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/package.json /app/pnpm-*.yaml /app/turbo.json ./
COPY --from=builder /app/server/package.json ./server/package.json
RUN pnpm install -F server --frozen-lockfile --prod
RUN pnpm dlx playwright install --with-deps chromium
RUN pnpm install --filter server --prod --frozen-lockfile --workspace-root
COPY --from=builder /app/server/dist ./server/dist
VOLUME /app/server/dist/assets/exports
VOLUME /app/server/dist/assets/uploads
EXPOSE 3100
@@ -52,4 +55,4 @@ ENV PORT 3100
HEALTHCHECK --interval=30s --timeout=20s --retries=3 --start-period=15s \
CMD curl -fSs localhost:3100/health || exit 1
CMD [ "pnpm", "run", "start:server" ]
CMD [ "pnpm", "run", "start", "--filter", "server" ]
+17 -18
View File
@@ -1,43 +1,40 @@
{
"name": "@reactive-resume/server",
"scripts": {
"dev": "nest start --watch",
"build": "rimraf dist && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint --fix --ext .ts ./src"
"debug": "nest start --debug --watch",
"start": "node dist/main"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.145.0",
"@aws-sdk/client-s3": "^3.154.0",
"@nestjs/axios": "^0.1.0",
"@nestjs/common": "^9.0.9",
"@nestjs/common": "^9.0.11",
"@nestjs/config": "^2.2.0",
"@nestjs/core": "^9.0.9",
"@nestjs/core": "^9.0.11",
"@nestjs/jwt": "^9.0.0",
"@nestjs/mapped-types": "^1.1.0",
"@nestjs/passport": "^9.0.0",
"@nestjs/platform-express": "^9.0.9",
"@nestjs/platform-express": "^9.0.11",
"@nestjs/schedule": "^2.1.0",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/terminus": "^9.1.1",
"@nestjs/typeorm": "^9.0.1",
"@sendgrid/mail": "^7.7.0",
"@types/passport": "^1.0.10",
"bcrypt": "^5.0.1",
"bcryptjs": "^2.4.3",
"cache-manager": "^4.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"cookie-parser": "^1.4.6",
"csvtojson": "^2.0.10",
"dayjs": "^1.11.4",
"google-auth-library": "^8.1.1",
"dayjs": "^1.11.5",
"google-auth-library": "^8.3.0",
"joi": "^17.6.0",
"lodash": "^4.17.21",
"multer": "^1.4.4",
"nanoid": "^3.3.4",
"node-stream-zip": "^1.15.0",
"nodemailer": "^6.7.8",
"passport": "^0.6.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
@@ -54,13 +51,15 @@
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.1",
"@reactive-resume/schema": "workspace:*",
"@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/lodash": "^4.14.182",
"@types/lodash": "^4.14.184",
"@types/multer": "^1.4.7",
"@types/node": "^18.7.1",
"eslint": "^8.21.0",
"@types/node": "^18.7.9",
"@types/nodemailer": "^6.4.5",
"@types/passport-jwt": "^3.0.6",
"@types/passport-local": "^1.0.34",
"prettier": "^2.7.1",
"source-map-support": "^0.5.21",
"ts-loader": "^9.3.1",
+17 -17
View File
@@ -2,8 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { SchedulerRegistry } from '@nestjs/schedule';
import bcrypt from 'bcrypt';
import { randomInt } from 'crypto';
import { compareSync, hashSync } from 'bcryptjs';
import { OAuth2Client } from 'google-auth-library';
import { PostgresErrorCode } from '@/database/errorCodes.enum';
@@ -24,12 +23,12 @@ export class AuthService {
) {}
async register(registerDto: RegisterDto) {
const hashedPassword = await bcrypt.hash(registerDto.password, randomInt(8, 12));
const password = hashSync(registerDto.password);
try {
const createdUser = await this.usersService.create({
...registerDto,
password: hashedPassword,
password,
provider: 'email',
});
@@ -43,6 +42,17 @@ export class AuthService {
}
}
async verifyPassword(password: string, hashedPassword: string) {
const isPasswordMatching = compareSync(password, hashedPassword);
if (!isPasswordMatching) {
throw new HttpException(
'The username/email and password combination provided was incorrect.',
HttpStatus.UNAUTHORIZED
);
}
}
async getUser(identifier: string, password: string) {
try {
const user = await this.usersService.findByIdentifier(identifier);
@@ -58,27 +68,17 @@ export class AuthService {
}
}
async verifyPassword(password: string, hashedPassword: string) {
const isPasswordMatching = await bcrypt.compare(password, hashedPassword);
if (!isPasswordMatching) {
throw new HttpException(
'The username/email and password combination provided was incorrect.',
HttpStatus.UNAUTHORIZED
);
}
}
forgotPassword(email: string) {
return this.usersService.generateResetToken(email);
}
async resetPassword(resetPasswordDto: ResetPasswordDto) {
const user = await this.usersService.findByResetToken(resetPasswordDto.resetToken);
const hashedPassword = await bcrypt.hash(resetPasswordDto.password, randomInt(8, 12));
const password = hashSync(resetPasswordDto.password);
await this.usersService.update(user.id, {
password: hashedPassword,
password,
resetToken: null,
});
+9 -7
View File
@@ -6,7 +6,7 @@ import appConfig from './app.config';
import authConfig from './auth.config';
import databaseConfig from './database.config';
import googleConfig from './google.config';
import sendgridConfig from './sendgrid.config';
import mailConfig from './mail.config';
import storageConfig from './storage.config';
const validationSchema = Joi.object({
@@ -36,11 +36,13 @@ const validationSchema = Joi.object({
GOOGLE_CLIENT_SECRET: Joi.string().allow(''),
PUBLIC_GOOGLE_CLIENT_ID: Joi.string().allow(''),
// SendGrid
SENDGRID_API_KEY: Joi.string().allow(''),
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID: Joi.string().allow(''),
SENDGRID_FROM_NAME: Joi.string().allow(''),
SENDGRID_FROM_EMAIL: Joi.string().allow(''),
// Mail
MAIL_FROM_NAME: Joi.string().allow(''),
MAIL_FROM_EMAIL: Joi.string().allow(''),
MAIL_HOST: Joi.string().allow(''),
MAIL_PORT: Joi.string().allow(''),
MAIL_USERNAME: Joi.string().allow(''),
MAIL_PASSWORD: Joi.string().allow(''),
// Storage
STORAGE_BUCKET: Joi.string().allow(''),
@@ -54,7 +56,7 @@ const validationSchema = Joi.object({
@Module({
imports: [
NestConfigModule.forRoot({
load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig, storageConfig],
load: [appConfig, authConfig, databaseConfig, googleConfig, mailConfig, storageConfig],
validationSchema: validationSchema,
}),
],
+12
View File
@@ -0,0 +1,12 @@
import { registerAs } from '@nestjs/config';
export default registerAs('mail', () => ({
from: {
name: process.env.MAIL_FROM_NAME,
email: process.env.MAIL_FROM_EMAIL,
},
host: process.env.MAIL_HOST,
port: process.env.MAIL_PORT,
username: process.env.MAIL_USERNAME,
password: process.env.MAIL_PASSWORD,
}));
-8
View File
@@ -1,8 +0,0 @@
import { registerAs } from '@nestjs/config';
export default registerAs('sendgrid', () => ({
apiKey: process.env.SENDGRID_API_KEY,
forgotPasswordTemplateId: process.env.SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID,
fromName: process.env.SENDGRID_FROM_NAME,
fromEmail: process.env.SENDGRID_FROM_EMAIL,
}));
+1 -3
View File
@@ -12,8 +12,6 @@ export class HealthController {
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
]);
return this.health.check([() => this.db.pingCheck('database')]);
}
}
+30
View File
@@ -0,0 +1,30 @@
import { Type } from 'class-transformer';
import { IsDefined, IsNotEmpty, IsString } from 'class-validator';
export class MailRecipient {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsString()
email: string;
}
export class SendMailDto {
@IsDefined()
@Type(() => MailRecipient)
from: MailRecipient;
@IsDefined()
@Type(() => MailRecipient)
to: MailRecipient;
@IsString()
@IsNotEmpty()
subject: string;
@IsString()
@IsNotEmpty()
message: string;
}
+34 -18
View File
@@ -1,40 +1,56 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import SendGrid from '@sendgrid/mail';
import { createTransport, Transporter } from 'nodemailer';
import { User } from '@/users/entities/user.entity';
import { SendMailDto } from './dto/send-mail.dto';
@Injectable()
export class MailService {
constructor(private configService: ConfigService) {
const sendGridApiKey = this.configService.get<string>('sendgrid.apiKey');
transporter: Transporter;
if (sendGridApiKey) {
SendGrid.setApiKey(this.configService.get<string>('sendgrid.apiKey'));
}
constructor(private configService: ConfigService) {
this.transporter = createTransport({
host: this.configService.get<string>('mail.host'),
port: this.configService.get<number>('mail.port'),
pool: true,
secure: false,
tls: { ciphers: 'SSLv3' },
auth: {
user: this.configService.get<string>('mail.username'),
pass: this.configService.get<string>('mail.password'),
},
});
}
async sendEmail(mail: SendGrid.MailDataRequired) {
return SendGrid.send(mail);
async sendEmail(sendMailDto: SendMailDto) {
this.transporter.sendMail({
from: `${sendMailDto.from.name} <${sendMailDto.from.email}>`,
to: `${sendMailDto.to.name} <${sendMailDto.to.email}>`,
subject: sendMailDto.subject,
text: sendMailDto.message,
html: sendMailDto.message,
});
}
async sendForgotPasswordEmail(user: User, resetToken: string): Promise<void> {
const appUrl = this.configService.get<string>('app.url');
const url = `${appUrl}?modal=auth.reset&resetToken=${resetToken}`;
const mailData: SendGrid.MailDataRequired = {
const sendMailDto: SendMailDto = {
from: {
name: this.configService.get<string>('sendgrid.fromName'),
email: this.configService.get<string>('sendgrid.fromEmail'),
name: this.configService.get<string>('mail.from.name'),
email: this.configService.get<string>('mail.from.email'),
},
to: user.email,
hideWarnings: true,
dynamicTemplateData: { url },
templateId: this.configService.get<string>('sendgrid.forgotPasswordTemplateId'),
to: {
name: user.name,
email: user.email,
},
subject: 'Reset your Reactive Resume password',
message: `<p>Hey ${user.name}!</p> <p>You can reset your password by visiting this link: <a href="${url}">${url}</a>.</p> <p>But hurry, because it will expire in 30 minutes.</p>`,
};
await SendGrid.send(mailData);
return;
await this.sendEmail(sendMailDto);
}
}
+3 -3
View File
@@ -2,7 +2,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { InjectRepository } from '@nestjs/typeorm';
import { randomBytes } from 'crypto';
import { Connection, Repository } from 'typeorm';
import { DataSource, Repository } from 'typeorm';
import { MailService } from '@/mail/mail.service';
@@ -19,7 +19,7 @@ export class UsersService {
@InjectRepository(User) private userRepository: Repository<User>,
private schedulerRegistry: SchedulerRegistry,
private mailService: MailService,
private connection: Connection
private dataSource: DataSource
) {}
async findById(id: number): Promise<User> {
@@ -93,7 +93,7 @@ export class UsersService {
const user = await this.findByEmail(email);
const resetToken = randomBytes(32).toString('hex');
const queryRunner = this.connection.createQueryRunner();
const queryRunner = this.dataSource.createQueryRunner();
const timeout = setTimeout(async () => {
await this.userRepository.update(user.id, { resetToken: null });
+15 -15
View File
@@ -3,33 +3,33 @@
"exclude": ["dist"],
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"sourceMap": true,
"declaration": true,
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"removeComments": true,
"strictNullChecks": false,
"resolveJsonModule": true,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noFallthroughCasesInSwitch": false,
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": false,
"paths": {
"@/auth/*": ["src/auth/*"],
"@/config/*": ["src/config/*"],
"@/constants/*": ["src/constants/*"],
"@/database/*": ["src/database/*"],
"@/decorators/*": ["src/decorators/*"],
"@/filters/*": ["src/filters/*"],
"@/mail/*": ["src/mail/*"],
"@/users/*": ["src/users/*"],
"@/config/*": ["src/config/*"],
"@/resume/*": ["src/resume/*"],
"@/users/*": ["src/users/*"]
"@/filters/*": ["src/filters/*"],
"@/database/*": ["src/database/*"],
"@/constants/*": ["src/constants/*"],
"@/decorators/*": ["src/decorators/*"]
}
}
}
+3 -3
View File
@@ -2,9 +2,9 @@
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"strict": true
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"$schema": "https://turborepo.org/schema.json",
"pipeline": {
"dev": {
"cache": false
},
"start": {
"cache": false
},
"build": {
"outputs": ["dist/**", ".next/**"],
"dependsOn": ["^build"]
}
},
"globalDependencies": [".env"]
}