feat: add credentials authentication (#1)
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
"dependencies": {
|
||||
"@alparr/auth": "workspace:^0.1.0",
|
||||
"@alparr/db": "workspace:^0.1.0",
|
||||
"@alparr/validation": "workspace:^0.1.0",
|
||||
"@trpc/client": "next",
|
||||
"@trpc/server": "next",
|
||||
"superjson": "2.2.1",
|
||||
@@ -34,4 +35,4 @@
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { authRouter } from "./router/auth";
|
||||
import { postRouter } from "./router/post";
|
||||
import { userRouter } from "./router/user";
|
||||
import { createTRPCRouter } from "./trpc";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
auth: authRouter,
|
||||
post: postRouter,
|
||||
user: userRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
getSession: publicProcedure.query(({ ctx }) => {
|
||||
return ctx.session;
|
||||
}),
|
||||
getSecretMessage: protectedProcedure.query(() => {
|
||||
// testing type validation of overridden next-auth Session in @alparr/auth package
|
||||
return "you can see this secret message!";
|
||||
}),
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { desc, eq, schema } from "@alparr/db";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
|
||||
export const postRouter = createTRPCRouter({
|
||||
all: publicProcedure.query(({ ctx }) => {
|
||||
// return ctx.db.select().from(schema.post).orderBy(desc(schema.post.id));
|
||||
return ctx.db.query.post.findMany({ orderBy: desc(schema.post.id) });
|
||||
}),
|
||||
|
||||
byId: publicProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(({ ctx, input }) => {
|
||||
// return ctx.db
|
||||
// .select()
|
||||
// .from(schema.post)
|
||||
// .where(eq(schema.post.id, input.id));
|
||||
|
||||
return ctx.db.query.post.findFirst({
|
||||
where: eq(schema.post.id, input.id),
|
||||
});
|
||||
}),
|
||||
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
title: z.string().min(1),
|
||||
content: z.string().min(1),
|
||||
}),
|
||||
)
|
||||
.mutation(({ ctx, input }) => {
|
||||
return ctx.db.insert(schema.post).values(input);
|
||||
}),
|
||||
|
||||
delete: protectedProcedure.input(z.number()).mutation(({ ctx, input }) => {
|
||||
return ctx.db.delete(schema.post).where(eq(schema.post.id, input));
|
||||
}),
|
||||
});
|
||||
39
packages/api/src/router/user.ts
Normal file
39
packages/api/src/router/user.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import "server-only";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createSalt, hashPassword } from "@alparr/auth";
|
||||
import { createId, schema } from "@alparr/db";
|
||||
import { initUserSchema } from "@alparr/validation";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
initUser: publicProcedure
|
||||
.input(initUserSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const firstUser = await ctx.db.query.users.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (firstUser) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "User already exists",
|
||||
});
|
||||
}
|
||||
|
||||
const salt = await createSalt();
|
||||
const hashedPassword = await hashPassword(input.password, salt);
|
||||
|
||||
const userId = createId();
|
||||
await ctx.db.insert(schema.users).values({
|
||||
id: userId,
|
||||
name: input.username,
|
||||
password: hashedPassword,
|
||||
salt,
|
||||
});
|
||||
}),
|
||||
});
|
||||
1
packages/auth/client.ts
Normal file
1
packages/auth/client.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { signIn, signOut } from "next-auth/react";
|
||||
76
packages/auth/configuration.ts
Normal file
76
packages/auth/configuration.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
import { db } from "@alparr/db";
|
||||
|
||||
import { credentialsConfiguration } from "./providers/credentials";
|
||||
import { EmptyNextAuthProvider } from "./providers/empty";
|
||||
import { expireDateAfter, generateSessionToken } from "./session";
|
||||
|
||||
const adapter = DrizzleAdapter(db);
|
||||
const sessionMaxAgeInSeconds = 30 * 24 * 60 * 60; // 30 days
|
||||
|
||||
export const createConfiguration = (isCredentialsRequest: boolean) =>
|
||||
NextAuth({
|
||||
adapter,
|
||||
providers: [Credentials(credentialsConfiguration), EmptyNextAuthProvider()],
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
},
|
||||
}),
|
||||
signIn: async ({ user }) => {
|
||||
if (!isCredentialsRequest) return true;
|
||||
|
||||
if (!user) return true;
|
||||
|
||||
const sessionToken = generateSessionToken();
|
||||
const sessionExpiry = expireDateAfter(sessionMaxAgeInSeconds);
|
||||
|
||||
// https://github.com/nextauthjs/next-auth/issues/6106
|
||||
if (!adapter?.createSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await adapter.createSession({
|
||||
sessionToken: sessionToken,
|
||||
userId: user.id,
|
||||
expires: sessionExpiry,
|
||||
});
|
||||
|
||||
cookies().set("next-auth.session-token", sessionToken, {
|
||||
path: "/",
|
||||
expires: sessionExpiry,
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "database",
|
||||
maxAge: sessionMaxAgeInSeconds,
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
error: "/auth/login",
|
||||
},
|
||||
jwt: {
|
||||
encode() {
|
||||
const cookie = cookies().get("next-auth.session-token")?.value;
|
||||
return cookie ?? "";
|
||||
},
|
||||
|
||||
decode() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -3,8 +3,6 @@ import { z } from "zod";
|
||||
|
||||
export const env = createEnv({
|
||||
server: {
|
||||
AUTH_DISCORD_ID: z.string().min(1),
|
||||
AUTH_DISCORD_SECRET: z.string().min(1),
|
||||
AUTH_SECRET:
|
||||
process.env.NODE_ENV === "production"
|
||||
? z.string().min(1)
|
||||
@@ -19,8 +17,6 @@ export const env = createEnv({
|
||||
},
|
||||
client: {},
|
||||
runtimeEnv: {
|
||||
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
|
||||
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
|
||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||
AUTH_URL: process.env.AUTH_URL,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* @see https://github.com/nextauthjs/next-auth/pull/8932 */
|
||||
|
||||
import Discord from "@auth/core/providers/discord";
|
||||
import type { DefaultSession } from "@auth/core/types";
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import NextAuth from "next-auth";
|
||||
|
||||
import { db, tableCreator } from "@alparr/db";
|
||||
import { createConfiguration } from "./configuration";
|
||||
|
||||
export type { Session } from "next-auth";
|
||||
|
||||
@@ -18,21 +12,8 @@ declare module "next-auth" {
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth({
|
||||
adapter: DrizzleAdapter(db, tableCreator),
|
||||
providers: [Discord],
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
export * from "./security";
|
||||
|
||||
export const createHandlers = (isCredentialsRequest: boolean) =>
|
||||
createConfiguration(isCredentialsRequest);
|
||||
export const { auth } = createConfiguration(false);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
"@auth/core": "^0.18.4",
|
||||
"@auth/drizzle-adapter": "^0.3.9",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cookies": "^0.8.0",
|
||||
"next": "^14.0.3",
|
||||
"next-auth": "5.0.0-beta.4",
|
||||
"react": "18.2.0",
|
||||
@@ -26,6 +28,9 @@
|
||||
"@alparr/eslint-config": "workspace:^0.2.0",
|
||||
"@alparr/prettier-config": "workspace:^0.1.0",
|
||||
"@alparr/tsconfig": "workspace:^0.1.0",
|
||||
"@alparr/validation": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.7.10",
|
||||
"eslint": "^8.53.0",
|
||||
"prettier": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
@@ -37,4 +42,4 @@
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
49
packages/auth/providers/credentials.ts
Normal file
49
packages/auth/providers/credentials.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type Credentials from "@auth/core/providers/credentials";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
import { db, eq } from "@alparr/db";
|
||||
import { users } from "@alparr/db/schema/sqlite";
|
||||
import { signInSchema } from "@alparr/validation";
|
||||
|
||||
type CredentialsConfiguration = Parameters<typeof Credentials>[0];
|
||||
|
||||
export const credentialsConfiguration = {
|
||||
type: "credentials",
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
name: {
|
||||
label: "Username",
|
||||
type: "text",
|
||||
},
|
||||
password: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const data = await signInSchema.parseAsync(credentials);
|
||||
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.name, data.name),
|
||||
});
|
||||
|
||||
if (!user?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`user ${user.name} is trying to log in. checking password...`);
|
||||
const isValidPassword = await bcrypt.compare(data.password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log(`password for user ${user.name} was incorrect`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`user ${user.name} successfully authorized`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
};
|
||||
},
|
||||
} satisfies CredentialsConfiguration;
|
||||
16
packages/auth/providers/empty.ts
Normal file
16
packages/auth/providers/empty.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { OAuthConfig } from "next-auth/providers";
|
||||
|
||||
export function EmptyNextAuthProvider(): OAuthConfig<unknown> {
|
||||
return {
|
||||
id: "empty",
|
||||
name: "Empty",
|
||||
type: "oauth",
|
||||
profile: () => {
|
||||
throw new Error(
|
||||
"EmptyNextAuthProvider can not be used and is only a placeholder because credentials authentication can not be used as session authentication without additional providers.",
|
||||
);
|
||||
},
|
||||
issuer: "empty",
|
||||
authorization: new URL("https://example.empty"),
|
||||
};
|
||||
}
|
||||
9
packages/auth/security.ts
Normal file
9
packages/auth/security.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export const createSalt = async () => {
|
||||
return bcrypt.genSalt(10);
|
||||
};
|
||||
|
||||
export const hashPassword = async (password: string, salt: string) => {
|
||||
return bcrypt.hash(password, salt);
|
||||
};
|
||||
9
packages/auth/session.ts
Normal file
9
packages/auth/session.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
export const expireDateAfter = (seconds: number) => {
|
||||
return new Date(Date.now() + seconds * 1000);
|
||||
};
|
||||
|
||||
export const generateSessionToken = () => {
|
||||
return randomUUID();
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import Database from "better-sqlite3";
|
||||
import { drizzle } from "drizzle-orm/better-sqlite3";
|
||||
|
||||
import * as sqliteSchema from "./schema/sqlite";
|
||||
|
||||
|
||||
export const schema = sqliteSchema;
|
||||
|
||||
export * from "drizzle-orm";
|
||||
@@ -11,3 +10,5 @@ export * from "drizzle-orm";
|
||||
const sqlite = new Database(process.env.DB_URL!);
|
||||
|
||||
export const db = drizzle(sqlite, { schema });
|
||||
|
||||
export { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/better-sqlite3": "^7.6.8",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"drizzle-orm": "^0.29.1"
|
||||
},
|
||||
@@ -22,6 +22,7 @@
|
||||
"@alparr/eslint-config": "workspace:^0.2.0",
|
||||
"@alparr/prettier-config": "workspace:^0.1.0",
|
||||
"@alparr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/better-sqlite3": "7.6.8",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
"drizzle-kit": "^0.20.6",
|
||||
"eslint": "^8.53.0",
|
||||
@@ -35,4 +36,4 @@
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +1,73 @@
|
||||
import type { AdapterAccount } from '@auth/core/adapters';
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
import { relations } from 'drizzle-orm';
|
||||
import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import type { AdapterAccount } from "@auth/core/adapters";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
index,
|
||||
integer,
|
||||
primaryKey,
|
||||
sqliteTable,
|
||||
text,
|
||||
} from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const users = sqliteTable('user', {
|
||||
id: text('id').notNull().primaryKey(),
|
||||
name: text('name'),
|
||||
email: text('email'),
|
||||
emailVerified: integer('emailVerified', { mode: 'timestamp_ms' }),
|
||||
image: text('image'),
|
||||
password: text('password'),
|
||||
salt: text('salt'),
|
||||
export const users = sqliteTable("user", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
name: text("name"),
|
||||
email: text("email"),
|
||||
emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
|
||||
image: text("image"),
|
||||
password: text("password"),
|
||||
salt: text("salt"),
|
||||
});
|
||||
|
||||
export const accounts = sqliteTable(
|
||||
'account',
|
||||
"account",
|
||||
{
|
||||
userId: text('userId')
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
type: text('type').$type<AdapterAccount['type']>().notNull(),
|
||||
provider: text('provider').notNull(),
|
||||
providerAccountId: text('providerAccountId').notNull(),
|
||||
refresh_token: text('refresh_token'),
|
||||
access_token: text('access_token'),
|
||||
expires_at: integer('expires_at'),
|
||||
token_type: text('token_type'),
|
||||
scope: text('scope'),
|
||||
id_token: text('id_token'),
|
||||
session_state: text('session_state'),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
type: text("type").$type<AdapterAccount["type"]>().notNull(),
|
||||
provider: text("provider").notNull(),
|
||||
providerAccountId: text("providerAccountId").notNull(),
|
||||
refresh_token: text("refresh_token"),
|
||||
access_token: text("access_token"),
|
||||
expires_at: integer("expires_at"),
|
||||
token_type: text("token_type"),
|
||||
scope: text("scope"),
|
||||
id_token: text("id_token"),
|
||||
session_state: text("session_state"),
|
||||
},
|
||||
(account) => ({
|
||||
compoundKey: primaryKey({ columns: [account.provider, account.providerAccountId] }),
|
||||
userIdIdx: index('userId_idx').on(account.userId),
|
||||
})
|
||||
compoundKey: primaryKey({
|
||||
columns: [account.provider, account.providerAccountId],
|
||||
}),
|
||||
userIdIdx: index("userId_idx").on(account.userId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const sessions = sqliteTable(
|
||||
'session',
|
||||
"session",
|
||||
{
|
||||
sessionToken: text('sessionToken').notNull().primaryKey(),
|
||||
userId: text('userId')
|
||||
sessionToken: text("sessionToken").notNull().primaryKey(),
|
||||
userId: text("userId")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
expires: integer('expires', { mode: 'timestamp_ms' }).notNull(),
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||
},
|
||||
(session) => ({
|
||||
userIdIdx: index('user_id_idx').on(session.userId),
|
||||
})
|
||||
userIdIdx: index("user_id_idx").on(session.userId),
|
||||
}),
|
||||
);
|
||||
|
||||
export const verificationTokens = sqliteTable(
|
||||
'verificationToken',
|
||||
"verificationToken",
|
||||
{
|
||||
identifier: text('identifier').notNull(),
|
||||
token: text('token').notNull(),
|
||||
expires: integer('expires', { mode: 'timestamp_ms' }).notNull(),
|
||||
identifier: text("identifier").notNull(),
|
||||
token: text("token").notNull(),
|
||||
expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
|
||||
},
|
||||
(vt) => ({
|
||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
export const accountRelations = relations(accounts, ({ one }) => ({
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './src';
|
||||
export * from "./src";
|
||||
|
||||
@@ -35,4 +35,4 @@
|
||||
"dependencies": {
|
||||
"@mantine/core": "^7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MantineProviderProps } from "@mantine/core";
|
||||
|
||||
import { theme } from "./theme";
|
||||
|
||||
|
||||
export const uiConfiguration = ({
|
||||
theme,
|
||||
}) satisfies MantineProviderProps;
|
||||
export const uiConfiguration = {
|
||||
theme,
|
||||
} satisfies MantineProviderProps;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { createTheme } from "@mantine/core";
|
||||
|
||||
import { primaryColor } from "./theme/colors/primary";
|
||||
import { secondaryColor } from "./theme/colors/secondary";
|
||||
|
||||
export const theme = createTheme({
|
||||
colors: {
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
},
|
||||
primaryColor: "primaryColor",
|
||||
});
|
||||
colors: {
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
},
|
||||
primaryColor: "primaryColor",
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { MantineColorsTuple } from "@mantine/core";
|
||||
|
||||
export const primaryColor: MantineColorsTuple = [
|
||||
'#eafbf0',
|
||||
'#ddefe3',
|
||||
'#bedcc7',
|
||||
'#9bc8aa',
|
||||
'#7eb892',
|
||||
'#6bad81',
|
||||
'#60a878',
|
||||
'#509265',
|
||||
'#438359',
|
||||
'#35724a'
|
||||
"#eafbf0",
|
||||
"#ddefe3",
|
||||
"#bedcc7",
|
||||
"#9bc8aa",
|
||||
"#7eb892",
|
||||
"#6bad81",
|
||||
"#60a878",
|
||||
"#509265",
|
||||
"#438359",
|
||||
"#35724a",
|
||||
];
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { MantineColorsTuple } from "@mantine/core";
|
||||
|
||||
export const secondaryColor: MantineColorsTuple = [
|
||||
'#e6f7ff',
|
||||
'#d9e8f6',
|
||||
'#b6cde6',
|
||||
'#90b2d4',
|
||||
'#6f9ac5',
|
||||
'#5a8bbd',
|
||||
'#4d84ba',
|
||||
'#3d71a4',
|
||||
'#326595',
|
||||
'#205885'
|
||||
];
|
||||
"#e6f7ff",
|
||||
"#d9e8f6",
|
||||
"#b6cde6",
|
||||
"#90b2d4",
|
||||
"#6f9ac5",
|
||||
"#5a8bbd",
|
||||
"#4d84ba",
|
||||
"#3d71a4",
|
||||
"#326595",
|
||||
"#205885",
|
||||
];
|
||||
|
||||
1
packages/validation/index.ts
Normal file
1
packages/validation/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
38
packages/validation/package.json
Normal file
38
packages/validation/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@alparr/validation",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@alparr/eslint-config": "workspace:^0.2.0",
|
||||
"@alparr/prettier-config": "workspace:^0.1.0",
|
||||
"@alparr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^8.53.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@alparr/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@alparr/prettier-config",
|
||||
"dependencies": {
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
1
packages/validation/src/index.ts
Normal file
1
packages/validation/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./user";
|
||||
20
packages/validation/src/user.ts
Normal file
20
packages/validation/src/user.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const usernameSchema = z.string().min(3).max(255);
|
||||
const passwordSchema = z.string().min(8).max(255);
|
||||
|
||||
export const initUserSchema = z
|
||||
.object({
|
||||
username: usernameSchema,
|
||||
password: passwordSchema,
|
||||
repeatPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.repeatPassword, {
|
||||
path: ["repeatPassword"],
|
||||
message: "Passwords do not match",
|
||||
});
|
||||
|
||||
export const signInSchema = z.object({
|
||||
name: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
8
packages/validation/tsconfig.json
Normal file
8
packages/validation/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@alparr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user