Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add initial ui for umamin social #229

Merged
merged 16 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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