如何使用Next.js、TypeORM和Shadcn-UI从零搭建现代化SaaS系统(完成登录和框架)

461 阅读4分钟

博客地址-藏经阁郭大爷


这个章节我们完成整个框架的搭建

分析缺少了什么东西

  1. 我们缺少登录页
  2. 我们缺少验证码发送逻辑
  3. 我们缺少了工作台页面

我们先做一下登录页

首先我们需要做一个登录页用的layout

// client/src/components/SlimLayout.tsx
import Image from 'next/image';

import backgroundImage from '@/images/background-auth.jpg';

export function SlimLayout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <div className="relative flex min-h-full shrink-0 justify-center md:px-12 lg:px-0 flex-1">
        <div className="relative z-10 flex flex-1 flex-col bg-white px-4 py-10 shadow-2xl sm:justify-center md:flex-none md:px-28">
          <main className="mx-auto w-full max-w-md sm:px-4 md:w-96 md:max-w-sm md:px-0">{children}</main>
        </div>
        <div className="hidden sm:contents lg:relative lg:block lg:flex-1">
          <Image className="absolute inset-0 h-full w-full object-cover" src={backgroundImage} alt="" unoptimized />
        </div>
      </div>
    </>
  );
}


#添加react-hook-form
pnpm add react-hook-form zod

// client/src/schema/index.ts
// 添加我们的schema
import { z } from 'zod';

export const loginFormSchema = z.object({
  email: z.string().email({
    message: '请填写正确的邮箱'
  }),
  code: z
    .string()
    .min(1, {
      message: '请输入验证码'
    })
    .min(6, {
      message: '请输入六位验证码'
    })
    .max(6, {
      message: '请输入六位验证码'
    })
});

export const emailCodeSchema = z.object({
  email: z.string().email({
    message: '请填写正确的邮箱'
  })
});

增加useCheckLogin和useCountDown和useMounted

这两个hooks一个是检测是否登录,一个是发送邮件验证码后,倒计时的,一个是判断客户端的

'use client';
import { useAppStore } from '@/components/providers/appStoreProvider';
import { useEffect } from 'react';

export const useCheckLogin = (callBack?: (isLogin: boolean) => void) => {
  const [isLogin] = useAppStore(state => [state.isLogin]);
  useEffect(() => {
    if (isLogin !== undefined) {
      callBack && callBack(isLogin);
    }
  }, [isLogin]);
  return {
    isLogin
  };
};


import { useEffect, useState } from 'react';

export const useCountDown = () => {
  const [count, setCount] = useState<number>(0);
  useEffect(() => {
    let timer: any;
    if (count > 0) {
      timer = setTimeout(() => {
        setCount(count - 1);
      }, 1000);
    }
    return () => {
      timer && clearTimeout(timer);
    };
  }, [count]);
  return {
    count,
    setCount,
  };
};


import { useEffect, useState } from 'react';

export const useMounted = () => {
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  return [mounted];
};


// apps/client/src/app/(auth)/_components/loginView.tsx
'use client';
import { useForm } from 'react-hook-form';
import { SlimLayout } from '@/components/SlimLayout';
import { Logo } from '@/components/Logo';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginFormSchema } from '@/schema';
import { trpc } from '@/app/_trpc/index';
import { Loader } from 'lucide-react';
import { useCheckLogin } from '@/hooks/useCheckLogin';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { useCountDown } from '@/hooks/useCountDown';
import { useInitContext } from '@/components/providers/initProvider';

export const LoginView = () => {
  const router = useRouter();
  const initContext = useInitContext();
  const { isLogin } = useCheckLogin(isLogin => {
    if (isLogin) {
      router.replace('/home');
    }
  });
  const { count, setCount } = useCountDown();
  const form = useForm<z.infer<typeof loginFormSchema>>({
    resolver: zodResolver(loginFormSchema),
    defaultValues: {
      email: '',
      code: ''
    }
  });
  const loginMutation = trpc.login.useMutation({
    onSuccess: async ctx => {
      if (ctx.success) {
        const url = new URL(window.location.href);
        localStorage.setItem('accessToken', ctx.data.accessToken);
        await initContext.refreshLogin();
        router.replace(url.searchParams.get('redirect') || '/');
      }
    },
    onError: (error, variables, context) => {
      toast(error.message);
    }
  });
  const getCodeMutation = trpc.getCode.useMutation({
    onSuccess: ctx => {
      if (ctx.success) {
        setCount(60);
      }
    },
    onError: (error, variables, context) => {
      toast(error.message);
    }
  });
  const onSubmit = async (values: z.infer<typeof loginFormSchema>) => {
    void loginMutation.mutate({
      ...values
    });
  };
  if (isLogin === undefined || isLogin) return null;
  return (
    <SlimLayout>
      <div className="flex">
        <Logo />
      </div>
      <h2 className="mt-20 text-lg font-semibold text-gray-900">登陆你的账号</h2>
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="mt-10 grid grid-cols-1 gap-y-8">
          <FormField
            control={form.control}
            render={({ field }) => {
              return (
                <FormItem className={'grid gap-2 relative'}>
                  <FormLabel>邮箱</FormLabel>
                  <FormControl>
                    <Input placeholder={'请输入邮箱'} {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              );
            }}
            name={'email'}
          />
          <FormField
            control={form.control}
            render={({ field }) => {
              return (
                <FormItem className={'grid gap-2'}>
                  <FormLabel>验证码</FormLabel>
                  <div className={'relative'}>
                    <FormControl className={'pr-32'}>
                      <Input placeholder={'请输入验证码'} maxLength={6} {...field} />
                    </FormControl>
                    <Button
                      variant={'ghost'}
                      type={'button'}
                      disabled={count > 0}
                      onClick={() => {
                        if (count > 0) return;
                        getCodeMutation.mutate({
                          email: form.getValues()['email']
                        });
                      }}
                      className={'absolute top-0 right-0 w-28 px-0'}>
                      {getCodeMutation.isPending && <Loader className={'animate-spin'}></Loader>}
                      {count == 0 ? '获取验证码' : `${count}S`}
                    </Button>
                  </div>
                  <FormMessage />
                </FormItem>
              );
            }}
            name={'code'}
          />
          <Button type="submit" variant="default" className="w-full">
            {loginMutation.isPending && <Loader className={'animate-spin'}></Loader>}
            <span>
              登录 <span aria-hidden="true">&rarr;</span>
            </span>
          </Button>
        </form>
      </Form>
    </SlimLayout>
  );
};

我们现在来完成trpc的接口

使用middleware来完成需要登录接口的保护

我们的登录没有使用cookie做session,我们是使用localstorage来保存token的。

获取用户信息,使用jsonwebtoken,我们这里需要使用到jose库

const secretKey = process.env.jwtSecretKey; 
const key = new TextEncoder().encode(secretKey);
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60; // 30 days
export async function encrypt(payload: any, maxAge = DEFAULT_MAX_AGE) {
  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime(Date.now() + maxAge)
    .sign(key);
}

export async function decrypt<T>(input: string): Promise<T> {
  const { payload } = await jwtVerify(input, key, {
    algorithms: ['HS256']
  });
  return payload as T;
}


import { headers } from 'next/headers';
import { entitiesTypesMap } from '@sass-startup/db';
import {Db} from "@/db";
import {decrypt} from "@/lib/utils";


export const getUser = async () => {
  try {
    const headersList = headers();
    const authorization = headersList.get('authorization') || '';
    let user: entitiesTypesMap<'SassStartUserEntity'> | null = null;
    if (authorization !== '') {
      const token = authorization.split(' ')[1] || '';
      if (token !== '') {
        //   解码得到user
        const userDe = await decrypt<{ user: entitiesTypesMap<'SassStartUserEntity'> }>(token);
        const db = await Db.install;
        user = await db.getRepository('SassStartUserEntity').findOne({
          where: {
            id: userDe.user.id
          }
        });
      }
    }
    return user;
  } catch (e) {
    return null;
  }
};

修改之前的trpc,增加一个中间件,获取用户,没有则返回401

import {initTRPC, TRPCError} from '@trpc/server';
import {getUser} from "@/server";



const t = initTRPC.create();
const middleware = t.middleware;

const isAuth = middleware(async opts => {
    const user = await getUser();
    if (!user) {
        throw new TRPCError({
            code: 'UNAUTHORIZED'
        });
    }
    return opts.next({
        ctx: {
            user: user
        }
    });
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const privateProcedure = t.procedure.use(isAuth);

登录接口,发送验证码,用户信息接口

import {privateProcedure, publicProcedure, router} from './trpc';
import {emailCodeSchema, loginFormSchema} from "@/schema";
import {RedisDb} from "@/db/redis";
import {IResponse} from "@/lib/res";
import {generateRandomStr,encrypt} from "@/lib/utils";
import nodemailer from 'nodemailer';
import {Db} from "@/db";
import {TRPCError} from "@trpc/server";
import {CustomBaseException} from "@/lib/error";

const transporter = nodemailer.createTransport({
    service: 'qq',
    host: 'smtp.qq.com',
    auth: {
        user: '424139777@qq.com',
        pass: '****'
    }
});

export const appRouter = router({
    // 登录
    login:publicProcedure.input(loginFormSchema).mutation(async ({ctx,input})=>{
        const { email, code } = input;
        const db = await Db.install;
        const redis = await RedisDb.install;
        const key = `${email}__${code}`;
        const hasCode = await redis.install.get(key);
        if (!hasCode) {
            throw new TRPCError({
                code: 'BAD_REQUEST',
                cause: new CustomBaseException(
                    {
                        message: '验证码错误,请重新输入',
                        meta: {
                            code: 40001,
                            data: 'tete'
                        }
                    },
                    400
                )
            });
        }
        // 删除
        void redis.install.del(key);
        // 判断是否存在
        let user = await db.getRepository('SassStartUserEntity').findOne({
            where: {
                email: email
            },
            withDeleted: true
        });
        if (user === null) {
            user = await db.getRepository('SassStartUserEntity').save({
                email: email,
                password: 'test'
            });
        }
        user.password = null as unknown as any;
        return IResponse.ok({
            accessToken: await encrypt({
                user
            })
        });
    }),
    // 发送邮箱验证码
    getCode: publicProcedure.input(emailCodeSchema).mutation(async ({ ctx, input }) => {
        const { email } = input;
        const redis = await RedisDb.install;
        const code = generateRandomStr(10, 6);
        await redis.install.setEx(`${email}__${code}`, code, 60 * 5);
        const mail = await transporter.sendMail({
            from: '424139777@qq.com',
            to: email,
            subject: `Website activity from ${email}`,
            html: `
            <p>你的验证码是: ${code} </p>
            `
        });
        transporter.close();
        return IResponse.ok({});
    }),
 //获取登录用户信息
    getUser: privateProcedure.query(async ({ ctx }) => {
        return {
            user: ctx.user
        };
    })
});

export type AppRouter = typeof appRouter;

添加初始化的provider,获取用户信息

'use client';
import { FC, PropsWithChildren, useEffect, createContext, useContext, useCallback } from 'react';
import { useAppStore } from '@/components/providers/appStoreProvider';
import { trpc } from '@/app/_trpc/index';
import { useMounted } from '@/hooks/useMounted';
import { useRouter } from 'next/navigation';

const initContext = createContext<{
    refreshLogin: () => Promise<void>;
    logOut: () => void;
}>({} as any);

type InitProviderProps = {} & PropsWithChildren;
export const InitProvider: FC<InitProviderProps> = ({ children }) => {
    const router = useRouter();
    const [, setUserProfile] = useAppStore(state => [state.isLogin, state.setUserProfile]);
    const [mounted] = useMounted();
    const { data, isError, refetch } = trpc.getUser.useQuery(undefined, {
        retry: 0,
        enabled: mounted
    });
    const refreshLogin = async () => {
        const c = await refetch();
        if (c.data) {
            setUserProfile(true, {
                email: c.data.user.email,
                nickName: c.data.user.nickName,
            });
        }
    };
    useEffect(() => {
        const listenStorage = async (event: StorageEvent) => {
            if (event.key === 'accessToken') {
                const c = await refetch();
            }
        };
        addEventListener('storage', listenStorage);
        return () => {
            removeEventListener('storage', listenStorage);
        };
    }, []);
    const logOut = useCallback(() => {
        localStorage.removeItem('accessToken');
        setUserProfile(false, {});
    }, []);
    useEffect(() => {
        if (mounted) {
            if (isError) {
                setUserProfile(false);
            } else if (data) {
                console.log(data);
                setUserProfile(true, {
                    email: data.user.email,
                    nickName: data.user.nickName,
                });
            }
        }
    }, [mounted, data, isError]);
    return (
        <initContext.Provider
            value={{
                refreshLogin: refreshLogin,
                logOut: logOut
            }}>
            {children}
        </initContext.Provider>
    );
};
export const useInitContext = () => {
    return useContext(initContext);
};

我们就会看到服务端发起请求,点击跳转到登录页

添加一个按钮这个会判断登录状态会render不一样的组件

'use client';
import { useCheckLogin } from '@/hooks/useCheckLogin';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';

export const WorkSpaceButton = () => {
  const { isLogin } = useCheckLogin();

  if (isLogin) {
    return (
      <Link
        className={cn(
          buttonVariants({
            variant: 'default'
          })
        )}
        href={'/home'}>
        工作台
      </Link>
    );
  }
  return (
    <Link
      className={cn(
        buttonVariants({
          variant: 'ghost'
        })
      )}
      href={'/login'}>
      登录
    </Link>
  );
};

点击按钮后跳转到登录接口,点击一下发送验证码,然后拿到验证码之后我们就去登陆

我们输入正确的验证码之后会跳转到首页去

下面我们来创建工作台的页面

这个页面就是纯code,我就快速带过了,直接上代码了,自己看吧。

sideBar

import { FC } from 'react';
import { Logo } from '@/components/Logo';
import Link from 'next/link';
import { cn } from '@/lib/utils';
import { buttonVariants } from '@/components/ui/button';
import UserButton from '@/components/UserButton';
type SideBarProps = {
  className?: string;
};
export const SideBar: FC<SideBarProps> = ({ className = '' }) => {
  return (
    <div className={cn('w-60 bg-black h-full absolute md:fixed text-white', className)}>
      <div className={'py-5 px-2.5 flex flex-col w-full'}>
        <div className={'w-full flex justify-center items-center'}>
          <Logo></Logo>
        </div>
        <div className={'flex flex-col gap-4 mt-4 px-4 flex-1'}>
          <Link
            href={'/home?x'}
            className={cn(
              buttonVariants({
                variant: 'ghost'
              }),
              'justify-stretch'
            )}>
            home
          </Link>
        </div>
        <UserButton />
      </div>
    </div>
  );
};

homePageLayout

'use client';
import { createContext, FC, PropsWithChildren, useState } from 'react';
import { Sheet, SheetContent } from '@/components/ui/sheet';
import { Menu, X } from 'lucide-react';
import * as React from 'react';

import { useCheckLogin } from '@/hooks/useCheckLogin';
import { useRouter } from 'next/navigation';
import { SideBar } from '@/components/layout/sideBar';
import { Logo } from '@/components/Logo';
import UserButton from '@/components/UserButton';
export const homeLayoutContext = createContext<{
  triggerSideBar: (open: boolean) => void;
}>({} as any);
type HomePageLayoutProps = {};
export const HomePageLayout: FC<PropsWithChildren<HomePageLayoutProps>> = ({ children }) => {
  const router = useRouter();
  const { isLogin } = useCheckLogin(isLogin => {
    if (!isLogin) {
      router.replace(`/login?redirect=${encodeURIComponent(window.location.href)}`);
    }
  });
  const [sideBarOpen, setSideBarOpen] = useState(false);
  if (!isLogin) return null;
  return (
    <homeLayoutContext.Provider
      value={{
        triggerSideBar: setSideBarOpen
      }}>
      <div className={'flex flex-col min-h-full flex-1'}>
        <SideBar className={'hidden md:flex'} />
        <Sheet
          open={sideBarOpen}
          modal
          onOpenChange={flag => {
            setSideBarOpen(flag);
          }}>
          <SheetContent hiddenCloseBtn side={'left'} className={'p-0 max-w-60 sm:max-w-60 outline-none'}>
            <div
              onClick={() => {
                setSideBarOpen(prev => !prev);
              }}
              className={
                'absolute right-4 top-4 rounded-sm opacity-70  disabled:pointer-events-none data-[state=open]:bg-secondary z-10 text-white bg-gray-400 cursor-pointer'
              }>
              <X className="h-4 w-4" />
              <span className="sr-only">Close</span>
            </div>
            <SideBar />
          </SheetContent>
        </Sheet>
        {/*mobile header*/}
        <div className={'flex md:hidden justify-between px-4 h-14 items-center'}>
          <div
            className={'cursor-pointer'}
            onClick={() => {
              setSideBarOpen(true);
            }}>
            <Menu />
          </div>
          <Logo></Logo>
          <div>
            <UserButton className={'flex'} />
          </div>
        </div>
        <div className={'ml-0 md:ml-60'}>
          <div>{children}</div>
        </div>
      </div>
    </homeLayoutContext.Provider>
  );
};

UserButton

'use client';
import React, { FC, useState } from 'react';
import { useAppStore } from '@/components/providers/appStoreProvider';
import { cn } from '@/lib/utils';
import {Loader, User} from 'lucide-react';
import { Dialog,  DialogContent } from '@/components/ui/dialog';
import {
  DropdownMenu,
  DropdownMenuTrigger,
  DropdownMenuContent,
  DropdownMenuItem
} from '@/components/ui/dropdown-menu';
import { useInitContext } from '@/components/providers/initProvider';
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {z} from 'zod'
import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {nickNameSchema} from "@/schema";
import {Input} from "@/components/ui/input";
import {Button} from "@/components/ui/button";
import {trpc} from "@/app/_trpc";
import {toast} from "sonner";


type UserButtonProps = {
  className?: string;
};
const UserButton: FC<UserButtonProps> = ({ className = '' }) => {
  const [profile] = useAppStore(state => [state.userProfile]);
  const [openSetting, setOpenSetting] = useState(false);
  const { logOut } = useInitContext();
  const form = useForm<z.infer<typeof nickNameSchema>>({
    resolver:zodResolver(nickNameSchema),
      defaultValues:{
        nickName: profile.nickName
      }
  })
    const changeUser = trpc.changeUser.useMutation({
        onSuccess: ctx => {
            if (ctx.success) {
                setOpenSetting(false)
                toast('修改成功')
            }
        },
        onError(error){
            toast(error.message)
        }
    })
  const onSubmit = (values:z.infer<typeof nickNameSchema>)=>{
        if(changeUser.isPending)return
      changeUser.mutate(values)
  }
  return (
    <div className={cn('w-full justify-center items-center  md:flex cursor-pointer', className)}>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <div className={'w-full'}>
            <div className={'hidden md:block text-ellipsis line-clamp-1 px-4'}>{profile.email}</div>
            <div className={'block md:hidden'}>
              <User />
            </div>
          </div>
        </DropdownMenuTrigger>
        <DropdownMenuContent side={'right'} className={'min-w-40'}>
          <DropdownMenuItem
            onClick={() => {
              setOpenSetting(prev => !prev);
            }}>
            <span>设置</span>
          </DropdownMenuItem>
          <DropdownMenuItem onClick={logOut}>
            <span>退出登陆</span>
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <Dialog open={openSetting} onOpenChange={setOpenSetting}>
        <DialogContent className="sm:max-w-md">
          <div className={'text-xl font-medium'}>修改账号信息</div>
          <Form {...form}>
            <form onSubmit={form.handleSubmit(onSubmit)} className="mt-10 grid grid-cols-1 gap-y-8">
              <FormField
                  control={form.control}
                  render={({ field }) => {
                    return (
                        <FormItem className={'grid gap-2 relative'}>
                          <FormLabel>nickname</FormLabel>
                          <FormControl>
                            <Input placeholder={'请输入昵称'} {...field} />
                          </FormControl>
                          <FormMessage />
                        </FormItem>
                    );
                  }}
                  name={'nickName'}
              />
                <Button type="submit" variant="default" className="w-full flex gap-2">
                    确定修改 <span aria-hidden="true">&rarr;</span>
                </Button>
            </form>
          </Form>
        </DialogContent>
      </Dialog>
    </div>
  );
};

export default UserButton;

WorkSpace

'use client';
import { useEffect } from 'react';
import { HomePageLayout } from '@/components/layout/homePage';
import 'react-loading-skeleton/dist/skeleton.css';
import Skeleton from 'react-loading-skeleton';
const HomePage = () => {
  useEffect(() => {
    document.title = 'home';
  }, []);
  return (
    <HomePageLayout>
      <div>
        <div className={'h-14 flex justify-between p-4 border-b border-gray-300 items-center'}>
          <span className={'text-xl font-medium'}>WorkSpace</span>
        </div>
        {/*这里根据业务获取数据了*/}
        <div className={'p-4 grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-2 md:gap-4'}>
          {[1, 2, 3, 4, 5].map(item => {
            return (
              <div key={item + ''} className={'w-full  '}>
                <Skeleton count={5} />
              </div>
            );
          })}
        </div>
      </div>
    </HomePageLayout>
  );
};

export default HomePage;

增加修改用户信息接口

import {privateProcedure, publicProcedure, router} from './trpc';
import {emailCodeSchema, loginFormSchema, nickNameSchema} from "@/schema";
import {RedisDb} from "@/db/redis";
import {IResponse} from "@/lib/res";
import {generateRandomStr,encrypt} from "@/lib/utils";
import nodemailer from 'nodemailer';
import {Db} from "@/db";
import {TRPCError} from "@trpc/server";
import {CustomBaseException} from "@/lib/error";

const transporter = nodemailer.createTransport({
    service: 'qq',
    host: 'smtp.qq.com',
    auth: {
        user: '424139777@qq.com',
        pass: '****'
    }
});

export const appRouter = router({
    // 登录
    login:publicProcedure.input(loginFormSchema).mutation(async ({ctx,input})=>{
        const { email, code } = input;
        const db = await Db.install;
        const redis = await RedisDb.install;
        const key = `${email}__${code}`;
        const hasCode = await redis.install.get(key);
        if (!hasCode) {
            throw new TRPCError({
                code: 'BAD_REQUEST',
                cause: new CustomBaseException(
                    {
                        message: '验证码错误,请重新输入',
                        meta: {
                            code: 40001,
                            data: 'tete'
                        }
                    },
                    400
                )
            });
        }
        // 删除
        void redis.install.del(key);
        // 判断是否存在
        let user = await db.getRepository('SassStartUserEntity').findOne({
            where: {
                email: email
            },
            withDeleted: true
        });
        if (user === null) {
            user = await db.getRepository('SassStartUserEntity').save({
                email: email,
                password: 'test',
                nickName:email
            });
        }
        user.password = null as unknown as any;
        return IResponse.ok({
            accessToken: await encrypt({
                user
            })
        });
    }),
    // 发送邮箱验证码
    getCode: publicProcedure.input(emailCodeSchema).mutation(async ({ ctx, input }) => {
        const { email } = input;
        const redis = await RedisDb.install;
        const code = generateRandomStr(10, 6);
        await redis.install.setEx(`${email}__${code}`, code, 60 * 5);
        const mail = await transporter.sendMail({
            from: '424139777@qq.com',
            to: email,
            subject: `Website activity from ${email}`,
            html: `
            <p>你的验证码是: ${code} </p>
            `
        });
        transporter.close();
        return IResponse.ok({});
    }),
    getUser: privateProcedure.query(async ({ ctx }) => {
        return {
            user: ctx.user
        };
    }),
    changeUser:privateProcedure.input(nickNameSchema).mutation(async ({ctx,input})=>{
       try{
           const {nickName} = input;
           const userId = ctx.user.id;
           const db = await Db.install;
           await db.getRepository('SassStartUserEntity').update(userId,{
               nickName:nickName
           })
           return IResponse.ok({})
       }catch (e){
           throw new TRPCError({
               code:"INTERNAL_SERVER_ERROR",
               cause: new CustomBaseException(
                   {
                       message: '未知错误,请稍后重试',
                       meta: {
                           code: 50001,
                       }
                   },
                   500
               )
           })
       }
    })
});

export type AppRouter = typeof appRouter;

如何配置smtp的发送邮件

打开各自邮箱的smtp转发设置,qq邮箱的如下设置

项目仓库地址:sass-startup

转载自:藏经阁郭大爷