Skip to content

Commit

Permalink
Merge pull request #229 from omsimos/social
Browse files Browse the repository at this point in the history
add initial ui for umamin social
  • Loading branch information
joshxfi authored Aug 18, 2024
2 parents 38bd90a + dc382ab commit f66f4f4
Show file tree
Hide file tree
Showing 52 changed files with 2,294 additions and 270 deletions.
10 changes: 9 additions & 1 deletion apps/social/next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@umamin/ui", "@umamin/server"],
pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
experimental: {
serverComponentsExternalPackages: ["@node-rs/argon2"],
},
compiler: {
removeConsole: process.env.NODE_ENV === "production",
},

transpilePackages: ["@umamin/ui", "@umamin/db", "@umamin/gql"],
images: {
remotePatterns: [
{
Expand Down
58 changes: 36 additions & 22 deletions apps/social/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,58 @@
"build": "next build",
"start": "next start",
"clean": "rm -rf ./node_modules .turbo .next",
"check-types": "tsc --noEmit",
"lint": "next lint"
"check-types": "tsc --noEmit && gql.tada check",
"lint": "next lint",
"gql:check": "gql.tada check",
"gql:generate-persisted": "gql.tada generate-persisted",
"gql:generate-schema": "gql.tada generate-schema http://localhost:3000/api/graphql"
},
"dependencies": {
"@graphql-yoga/plugin-apq": "^3.3.0",
"@graphql-yoga/plugin-csrf-prevention": "^3.3.0",
"@graphql-yoga/plugin-disable-introspection": "^2.3.0",
"@fingerprintjs/botd": "^1.9.1",
"@graphql-yoga/plugin-csrf-prevention": "^3.6.2",
"@graphql-yoga/plugin-disable-introspection": "^2.6.2",
"@graphql-yoga/plugin-persisted-operations": "^3.6.2",
"@graphql-yoga/plugin-response-cache": "^3.8.2",
"@lucia-auth/adapter-drizzle": "^1.0.7",
"@node-rs/argon2": "^1.8.3",
"@umamin/db": "workspace:*",
"@umamin/gql": "workspace:*",
"@umamin/ui": "workspace:*",
"@lucia-auth/adapter-drizzle": "^1.0.7",
"@urql/core": "^5.0.3",
"@urql/exchange-graphcache": "^7.0.2",
"@urql/core": "^5.0.5",
"@urql/exchange-graphcache": "^7.1.1",
"@urql/exchange-persisted": "^4.3.0",
"@urql/next": "^1.1.1",
"@whatwg-node/server": "^0.9.46",
"arctic": "^1.8.1",
"geist": "^1.3.0",
"gql.tada": "^1.7.5",
"graphql": "^16.8.1",
"graphql-yoga": "^5.3.1",
"nanoid": "^5.0.7",
"oslo": "^1.2.0",
"urql": "^4.1.0",
"date-fns": "^3.6.0",
"geist": "^1.3.1",
"gql.tada": "^1.8.5",
"graphql": "^16.9.0",
"graphql-yoga": "^5.6.2",
"lucia": "^3.2.0",
"next": "14.2.3",
"lucide-react": "^0.424.0",
"nanoid": "^5.0.7",
"next": "14.2.5",
"nextjs-toploader": "^1.6.12",
"oslo": "^1.2.0",
"react": "^18",
"react-dom": "^18",
"sonner": "^1.4.41"
"sonner": "^1.5.0",
"urql": "^4.1.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@umamin/eslint-config": "workspace:*",
"@umamin/tsconfig": "workspace:*",
"@0no-co/graphqlsp": "^1.12.12",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@umamin/eslint-config": "workspace:*",
"@umamin/tsconfig": "workspace:*",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"postcss": "^8",
"tailwindcss": "^3.4.1"
"eslint-config-next": "14.2.5",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4"
}
}
1 change: 1 addition & 0 deletions apps/social/postcss.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require("@umamin/ui/postcss.config");
8 changes: 0 additions & 8 deletions apps/social/postcss.config.mjs

This file was deleted.

26 changes: 26 additions & 0 deletions apps/social/public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
259 changes: 259 additions & 0 deletions apps/social/src/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
"use server";

import { nanoid } from "nanoid";
import { db, eq } from "@umamin/db";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { hash, verify } from "@node-rs/argon2";
import {
user as userSchema,
account as accountSchema,
} from "@umamin/db/schema/user";
import { note as noteSchema } from "@umamin/db/schema/note";
import { message as messageSchema } from "@umamin/db/schema/message";

import { getSession, lucia } from "./lib/auth";
import { z } from "zod";

export async function logout(): Promise<ActionResult> {
const { session } = await getSession();

if (!session) {
throw new Error("Unauthorized");
}

await lucia.invalidateSession(session.id);

const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);

return redirect("/login");
}

const signupSchema = z
.object({
username: z
.string()
.min(5, {
message: "Username must be at least 5 characters",
})
.max(20, {
message: "Username must not exceed 20 characters",
})
.refine((url) => /^[a-zA-Z0-9_-]+$/.test(url), {
message: "Username must be alphanumeric with no spaces",
}),
password: z
.string()
.min(5, {
message: "Password must be at least 5 characters",
})
.max(255, {
message: "Password must not exceed 255 characters",
}),
confirmPassword: z.string(),
})
.refine(
(values) => {
return values.password === values.confirmPassword;
},
{
message: "Password does not match",
path: ["confirmPassword"],
},
);

export async function signup(_: any, formData: FormData) {
const validatedFields = signupSchema.safeParse({
username: formData.get("username"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
});

if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}

const passwordHash = await hash(validatedFields.data.password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});

const userId = nanoid();

try {
await db.insert(userSchema).values({
id: userId,
username: validatedFields.data.username.toLowerCase(),
passwordHash,
});
} catch (err: any) {
if (err.code === "SQLITE_CONSTRAINT") {
if (err.message.includes("user.username")) {
return {
errors: {
username: ["Username already taken"],
},
};
}
}

throw new Error("Something went wrong");
}

const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);

cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);

return redirect("/");
}

export async function login(_: any, formData: FormData): Promise<ActionResult> {
const username = formData.get("username");

if (
typeof username !== "string" ||
username.length < 5 ||
username.length > 20 ||
!/^[a-zA-Z0-9_-]+$/.test(username)
) {
return {
error: "Incorrect username or password",
};
}

const password = formData.get("password");

if (
typeof password !== "string" ||
password.length < 5 ||
password.length > 255
) {
return {
error: "Incorrect username or password",
};
}

const existingUser = await db.query.user.findFirst({
where: eq(userSchema.username, username.toLowerCase()),
});

if (!existingUser || !existingUser.passwordHash) {
return {
error: "Incorrect username or password",
};
}

const validPassword = await verify(existingUser.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});

if (!validPassword) {
return {
error: "Incorrect username or password",
};
}

const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);

return redirect("/");
}

export async function updatePassword({
currentPassword,
password,
}: {
currentPassword?: string;
password: string;
}): Promise<ActionResult> {
const { user } = await getSession();

if (!user) {
throw new Error("Unauthorized");
}

if (currentPassword && user.passwordHash) {
const validPassword = await verify(user.passwordHash, currentPassword, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});

if (!validPassword) {
return {
error: "Incorrect password",
};
}
}

const passwordHash = await hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1,
});

await db
.update(userSchema)
.set({ passwordHash })
.where(eq(userSchema.id, user.id));

return redirect("/settings");
}

export async function deleteAccount() {
const { user } = await getSession();

if (!user) {
throw new Error("Unauthorized");
}

try {
await db.batch([
db.delete(messageSchema).where(eq(messageSchema.receiverId, user.id)),
db.delete(accountSchema).where(eq(accountSchema.userId, user.id)),
db.delete(noteSchema).where(eq(noteSchema.userId, user.id)),
db.delete(userSchema).where(eq(userSchema.id, user.id)),
]);

await lucia.invalidateSession(user.id);

const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
} catch (err) {
throw new Error("Failed to delete account");
}

return redirect("/login");
}

interface ActionResult {
error: string | null;
}
Loading

0 comments on commit f66f4f4

Please sign in to comment.