Skip to content

Commit

Permalink
feat: google login for native apps
Browse files Browse the repository at this point in the history
  • Loading branch information
homocodian committed Aug 17, 2024
1 parent 1302a4a commit 01f2053
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 18 deletions.
2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@notes/backend",
"version": "0.0.9",
"version": "0.0.10",
"scripts": {
"dev": "NODE_DEBUG=* NODE_ENV=development bun --watch src/index.ts",
"build": "NODE_ENV=production bun build --target=bun --sourcemap=linked ./src/index.ts --outdir ./dist",
Expand Down
145 changes: 145 additions & 0 deletions apps/backend/src/v1/controllers/auth/create-google-oauth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import * as Sentry from "@sentry/bun";
import { and, eq } from "drizzle-orm";
import { Context } from "elysia";

import { db } from "@/db";
import { UserTable, oauthAccountTable, userTable } from "@/db/schema/user";
import { lucia } from "@/libs/auth";
import { BgQueue } from "@/libs/background-worker";
import { SaveDeviceProps } from "@/libs/background/save-device";
import { signJwtAsync } from "@/libs/jwt";
import { Prettify } from "@/types/prettify";
import { OAuthBodySchema, OAuthUserSchema } from "@/v1/validations/auth";
import { UserSchema } from "@/v1/validations/user";

interface CreateGoogleOAuthProps extends Context {
body: OAuthBodySchema;
ip: string;
}

export async function createGoogleOAuth({
ip,
body,
error,
request
}: CreateGoogleOAuthProps) {
try {
const { user, error: dbError } = await db.transaction(async (trx) => {
const [existingUser] = await trx
.select()
.from(oauthAccountTable)
.where(
and(
eq(oauthAccountTable.providerId, "google"),
eq(oauthAccountTable.providerUserId, body.user.id)
)
)
.innerJoin(userTable, eq(oauthAccountTable.userId, userTable.id));

if (!existingUser) {
const [createdUser] = await trx
.insert(userTable)
.values({
...body.user,
id: undefined,
disabled: false
})
.returning();

if (!createdUser) {
trx.rollback();
return { user: null, error: "Failed to create user" };
}

const [createdOauthAccount] = await trx
.insert(oauthAccountTable)
.values({
providerId: "google",
providerUserId: body.user.id,
userId: createdUser.id
})
.returning();

if (!createdOauthAccount) {
trx.rollback();
return { user: null, error: "Failed to create oauth account" };
}

return { user: createdUser, error: null };
} else {
const [updatedUser] = await trx
.update(userTable)
.set(getUserPropToUpdate(existingUser.user, body.user))
.where(eq(userTable.id, existingUser.user.id))
.returning();

if (!updatedUser) {
trx.rollback();
return { user: null, error: "Failed to update user" };
}

return { user: updatedUser, error: null };
}
});

if (!user) {
return error(500, dbError);
}

const session = await lucia.createSession(user.id, {});

const sessionToken = await signJwtAsync(session.id);

await BgQueue.add("saveDevice", {
ip,
ua: request.headers.get("user-agent") ?? undefined,
userId: user.id,
sessionId: session.id,
device: body.device
} satisfies SaveDeviceProps).catch((err) => {
console.log("🚀 ~ loginUser saveDevice ~ err", err);
Sentry.captureException(err);
});

const sessionUser = {
id: user.id,
email: user.email,
emailVerified: user.emailVerified,
photoURL: user.photoURL,
displayName: user.displayName,
sessionToken: sessionToken
} satisfies UserSchema & { sessionToken: string };

return sessionUser;
} catch (err) {
if ((err as any)?.code === "23505") {
return error(400, "User already exists");
}

return error("Internal Server Error");
}
}

type UserPropToUpdate = Partial<
Pick<UserTable, "emailVerified" | "photoURL" | "displayName" | "lastSignInAt">
>;

function getUserPropToUpdate(user: UserTable, googleUser: OAuthUserSchema) {
const propsToUpdate: Prettify<UserPropToUpdate> = {};

if (!user.emailVerified === false) {
propsToUpdate.emailVerified = googleUser.emailVerified;
}

if (!user.displayName) {
propsToUpdate.displayName = googleUser.displayName;
}

if (!user.photoURL) {
propsToUpdate.photoURL = googleUser.photoURL;
}

propsToUpdate.lastSignInAt = new Date();

return propsToUpdate;
}
8 changes: 2 additions & 6 deletions apps/backend/src/v1/controllers/user/google-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ import { SaveDeviceProps } from "@/libs/background/save-device";
import { signJwtAsync } from "@/libs/jwt";
import { parseJson } from "@/libs/parse-json";
import { Prettify } from "@/types/prettify";
import {
DeviceSchema,
UserResponse,
deviceSchema
} from "@/v1/validations/user";
import { DeviceSchema, UserSchema, deviceSchema } from "@/v1/validations/user";

interface GoogleCallbackProps extends Context {}

Expand Down Expand Up @@ -190,7 +186,7 @@ export async function googleCallback({
emailVerified: user.emailVerified,
photoURL: user.photoURL,
displayName: user.displayName
} satisfies UserResponse;
} satisfies UserSchema;

return redirect(
`${redirectURL}?sessionToken=${sessionToken}&user=${encodeURIComponent(JSON.stringify(sessionUser))}`
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/v1/controllers/user/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { lucia } from "@/libs/auth";
import { BgQueue } from "@/libs/background-worker";
import { SaveDeviceProps } from "@/libs/background/save-device";
import { signJwtAsync } from "@/libs/jwt";
import { LoginUser, UserResponse } from "@/v1/validations/user";
import { LoginUser, UserSchema } from "@/v1/validations/user";

interface LoginUserProps extends Context {
body: LoginUser;
Expand Down Expand Up @@ -66,7 +66,7 @@ export async function loginUser({ body, error, request, ip }: LoginUserProps) {
photoURL: user.photoURL,
displayName: user.displayName,
sessionToken
} satisfies UserResponse & { sessionToken: string };
} satisfies UserSchema & { sessionToken: string };
} catch (err) {
console.error("🚀 ~ loginUser ~ err:", err);
Sentry.captureException(err);
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/v1/controllers/user/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { lucia } from "@/libs/auth";
import { BgQueue } from "@/libs/background-worker";
import { UpdateSessionLastUsedAtProps } from "@/libs/background/update-session-last-used";
import { VerifyJwtAsync } from "@/libs/jwt";
import { UserResponse } from "@/v1/validations/user";
import { UserSchema } from "@/v1/validations/user";

interface GetProfileProps extends Context {
bearer: string | undefined;
Expand Down Expand Up @@ -41,7 +41,7 @@ export async function getProfile({ bearer, error }: GetProfileProps) {
emailVerified: user.emailVerified,
photoURL: user.photoURL,
displayName: user.displayName
} satisfies UserResponse;
} satisfies UserSchema;
} catch (err) {
console.log("🚀 ~ getProfile ~ err:", err);
Sentry.captureException(err);
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/v1/controllers/user/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { type SendVerificationCodeProps } from "@/libs/background/send-verificat
import { JwtError, signJwtAsync } from "@/libs/jwt";
import { validatePassword } from "@/libs/password-validation";
import { isValidEmail } from "@/v1/validations/email";
import { RegisterUser, UserResponse } from "@/v1/validations/user";
import { RegisterUser, UserSchema } from "@/v1/validations/user";

interface RegisterUserProps extends Context {
body: RegisterUser;
Expand Down Expand Up @@ -87,7 +87,7 @@ export async function registerUser({
photoURL: user.photoURL,
displayName: user.displayName,
sessionToken
} satisfies UserResponse & { sessionToken: string };
} satisfies UserSchema & { sessionToken: string };
} catch (err) {
console.log("🚀 ~ registerUser ~ err:", err);
Sentry.captureException(err);
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/v1/controllers/user/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { User } from "lucia";

import { db } from "@/db";
import { userTable } from "@/db/schema/user";
import type { UserResponse, UserUpdateSchema } from "@/v1/validations/user";
import type { UserSchema, UserUpdateSchema } from "@/v1/validations/user";

interface UpdateUserProps extends Context {
user: User;
Expand All @@ -30,7 +30,7 @@ export async function updateUser({ user, error, body }: UpdateUserProps) {
photoURL: updatedUser.photoURL,
displayName: updatedUser.displayName,
emailVerified: updatedUser.emailVerified
} satisfies UserResponse;
} satisfies UserSchema;
} catch (err) {
console.error("🚀 ~ updateUser ~ err:", err);
Sentry.captureException(err);
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/v1/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
registerUserSchema
} from "@/v1/validations/user";

import { createGoogleOAuth } from "../controllers/auth/create-google-oauth";
import { emailVerification } from "../controllers/user/email-verification";
import { googleCallback } from "../controllers/user/google-callback";
import { googleLogin } from "../controllers/user/google-login";
Expand All @@ -25,6 +26,7 @@ import { registerUser } from "../controllers/user/register";
import { sendVerificationEmail } from "../controllers/user/send-verification-email";
import { errorHandlerInstance } from "../utils/error-handler";
import { deriveUser } from "../utils/note/derive-user";
import { oAuthBodySchema } from "../validations/auth";

export const authRoute = new Elysia({ prefix: "/auth" })
.use(errorHandlerInstance)
Expand All @@ -43,6 +45,7 @@ export const authRoute = new Elysia({ prefix: "/auth" })
body: loginUserSchema
})
.get("/google", googleLogin)
.post("/google", createGoogleOAuth, { body: oAuthBodySchema })
.get("/google/callback", googleCallback, { query: oAuthQuerySchema })
.post("/logout", logout, { body: logoutSchema })
.post("/logout-all", logoutAll)
Expand Down
17 changes: 17 additions & 0 deletions apps/backend/src/v1/validations/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { t } from "elysia";

import { deviceSchema, userSchema } from "./user";

export const oAuthUserScheme = t.Composite([
t.Omit(userSchema, ["id"]),
t.Object({ id: t.String() })
]);

export type OAuthUserSchema = typeof oAuthUserScheme.static;

export const oAuthBodySchema = t.Object({
user: oAuthUserScheme,
device: deviceSchema
});

export type OAuthBodySchema = typeof oAuthBodySchema.static;
6 changes: 3 additions & 3 deletions apps/backend/src/v1/validations/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ export const passwordResetSchema = t.Object({

export type PasswordReset = typeof passwordResetSchema.static;

export const userResponseSchema = t.Object({
export const userSchema = t.Object({
id: t.Number(),
email: t.String(),
email: t.String({ format: "email" }),
emailVerified: t.Boolean(),
photoURL: t.Nullable(t.String()),
displayName: t.Nullable(t.String())
});

export type UserResponse = typeof userResponseSchema.static;
export type UserSchema = typeof userSchema.static;

export const emailVerificationSchema = t.Object({ code: t.String() });

Expand Down

0 comments on commit 01f2053

Please sign in to comment.