feat: added drizzle and oauth support
parent
39f42ade64
commit
1717e24563
@ -0,0 +1,11 @@
|
||||
import type { Config } from 'drizzle-kit'
|
||||
|
||||
export default {
|
||||
schema: './src/db/schema.ts',
|
||||
dialect: 'sqlite',
|
||||
driver: 'turso',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
authToken: process.env.DATABASE_AUTH_TOKEN,
|
||||
},
|
||||
} satisfies Config
|
||||
@ -0,0 +1,43 @@
|
||||
CREATE TABLE `login_logs` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`session_id` text,
|
||||
`user_id` text,
|
||||
`browser` text NOT NULL,
|
||||
`device` text NOT NULL,
|
||||
`os` text NOT NULL,
|
||||
`ip` text NOT NULL,
|
||||
`logged_in_at` text DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`session_id`) REFERENCES `sessions`(`id`) ON UPDATE no action ON DELETE set null,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `oauth_tokens` (
|
||||
`user_id` text NOT NULL,
|
||||
`strategy` text NOT NULL,
|
||||
`access_token` text NOT NULL,
|
||||
`refresh_token` text NOT NULL,
|
||||
`created_at` text DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(`user_id`, `strategy`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`userId` text,
|
||||
`expires_at` integer NOT NULL,
|
||||
FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `users` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`full_name` text,
|
||||
`user_name` text,
|
||||
`email` text NOT NULL,
|
||||
`profile_photo` text,
|
||||
`is_blocked` integer DEFAULT false,
|
||||
`is_deleted` integer DEFAULT false,
|
||||
`created_at` text DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_user_name_unique` ON `users` (`user_name`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
|
||||
@ -0,0 +1,304 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "e37882ca-80f0-4a5e-8f8a-07dc725fc5f9",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"login_logs": {
|
||||
"name": "login_logs",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"session_id": {
|
||||
"name": "session_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"browser": {
|
||||
"name": "browser",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device": {
|
||||
"name": "device",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"os": {
|
||||
"name": "os",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"ip": {
|
||||
"name": "ip",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"logged_in_at": {
|
||||
"name": "logged_in_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"login_logs_session_id_sessions_id_fk": {
|
||||
"name": "login_logs_session_id_sessions_id_fk",
|
||||
"tableFrom": "login_logs",
|
||||
"tableTo": "sessions",
|
||||
"columnsFrom": [
|
||||
"session_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"login_logs_user_id_users_id_fk": {
|
||||
"name": "login_logs_user_id_users_id_fk",
|
||||
"tableFrom": "login_logs",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"oauth_tokens": {
|
||||
"name": "oauth_tokens",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"strategy": {
|
||||
"name": "strategy",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"oauth_tokens_user_id_users_id_fk": {
|
||||
"name": "oauth_tokens_user_id_users_id_fk",
|
||||
"tableFrom": "oauth_tokens",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"oauth_tokens_user_id_strategy_pk": {
|
||||
"columns": [
|
||||
"user_id",
|
||||
"strategy"
|
||||
],
|
||||
"name": "oauth_tokens_user_id_strategy_pk"
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sessions_userId_users_id_fk": {
|
||||
"name": "sessions_userId_users_id_fk",
|
||||
"tableFrom": "sessions",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"full_name": {
|
||||
"name": "full_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_name": {
|
||||
"name": "user_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"profile_photo": {
|
||||
"name": "profile_photo",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_blocked": {
|
||||
"name": "is_blocked",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"is_deleted": {
|
||||
"name": "is_deleted",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_user_name_unique": {
|
||||
"name": "users_user_name_unique",
|
||||
"columns": [
|
||||
"user_name"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1727890502213,
|
||||
"tag": "0000_acoustic_marvel_zombies",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,10 @@
|
||||
import { drizzle } from 'drizzle-orm/libsql'
|
||||
import { createClient } from '@libsql/client'
|
||||
import * as schema from './schema'
|
||||
|
||||
const client = createClient({
|
||||
url: import.meta.env.DATABASE_URL,
|
||||
authToken: import.meta.env.DATABASE_AUTH_TOKEN,
|
||||
})
|
||||
|
||||
export const db = drizzle(client, { schema })
|
||||
@ -0,0 +1,97 @@
|
||||
import { relations, sql } from 'drizzle-orm'
|
||||
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
||||
|
||||
import { customAlphabet } from 'nanoid'
|
||||
|
||||
const createSessionId = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_-', 48)
|
||||
|
||||
export const users = sqliteTable('users', {
|
||||
id: text('id')
|
||||
.$default(() => createId())
|
||||
.primaryKey(),
|
||||
fullName: text('full_name'),
|
||||
userName: text('user_name').unique(),
|
||||
email: text('email').notNull().unique(),
|
||||
profilePhoto: text('profile_photo'),
|
||||
isBlocked: integer('is_blocked', { mode: 'boolean' }).default(false),
|
||||
isDeleted: integer('is_deleted', { mode: 'boolean' }).default(false),
|
||||
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
})
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
oauthTokens: many(oauthTokens),
|
||||
sessions: many(sessions),
|
||||
loginLogs: many(loginLogs),
|
||||
}))
|
||||
|
||||
export const oauthTokens = sqliteTable(
|
||||
'oauth_tokens',
|
||||
{
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id),
|
||||
strategy: text('strategy', { enum: ['google'] }).notNull(),
|
||||
accessToken: text('access_token').notNull(),
|
||||
refreshToken: text('refresh_token').notNull(),
|
||||
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
},
|
||||
table => {
|
||||
return {
|
||||
pk: primaryKey({ columns: [table.userId, table.strategy] }),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const oauthTokenRelations = relations(oauthTokens, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [oauthTokens.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const sessions = sqliteTable('sessions', {
|
||||
id: text('id')
|
||||
.$default(() => createSessionId())
|
||||
.primaryKey(),
|
||||
userId: text('userId').references(() => users.id, { onDelete: 'cascade' }),
|
||||
expiresAt: integer('expires_at').notNull(),
|
||||
})
|
||||
|
||||
export const sessionRelations = relations(sessions, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [sessions.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
loginLog: one(loginLogs),
|
||||
}))
|
||||
|
||||
export const loginLogs = sqliteTable('login_logs', {
|
||||
id: text('id')
|
||||
.$default(() => createId())
|
||||
.primaryKey(),
|
||||
sessionId: text('session_id').references(() => sessions.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
userId: text('user_id').references(() => users.id, {
|
||||
onDelete: 'cascade',
|
||||
}),
|
||||
|
||||
browser: text('browser').notNull(),
|
||||
device: text('device').notNull(),
|
||||
os: text('os').notNull(),
|
||||
ip: text('ip').notNull(),
|
||||
loggedInAt: text('logged_in_at').default(sql`CURRENT_TIMESTAMP`),
|
||||
})
|
||||
|
||||
export const loginLogsRelations = relations(loginLogs, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [loginLogs.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
session: one(sessions, {
|
||||
fields: [loginLogs.sessionId],
|
||||
references: [sessions.id],
|
||||
}),
|
||||
}))
|
||||
@ -1 +1,7 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
|
||||
declare namespace App {
|
||||
interface Locals {
|
||||
userId: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,139 @@
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import Bowser from 'bowser'
|
||||
|
||||
import { db } from '../db'
|
||||
import { loginLogs, oauthTokens, sessions, users } from '../db/schema'
|
||||
|
||||
type NewUserArgs = {
|
||||
email: string
|
||||
userName: string
|
||||
fullName: string
|
||||
profilePhoto: string
|
||||
}
|
||||
|
||||
type UserExistArgs = {
|
||||
email: string
|
||||
strategy: 'google'
|
||||
}
|
||||
|
||||
type NewSessionArgs = {
|
||||
userId: string
|
||||
}
|
||||
|
||||
type NewLogsArgs = {
|
||||
userAgent: string | null
|
||||
userId: string
|
||||
sessionId: string
|
||||
ip: string
|
||||
}
|
||||
|
||||
type TokenArgs = {
|
||||
userId: string
|
||||
strategy: 'google'
|
||||
refreshToken: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 14)
|
||||
|
||||
export async function throwError() {
|
||||
throw new Error('wtf')
|
||||
}
|
||||
|
||||
export const createUser = async ({ email, fullName, profilePhoto, userName }: NewUserArgs) => {
|
||||
try {
|
||||
const newUser = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
email,
|
||||
profilePhoto,
|
||||
fullName,
|
||||
userName,
|
||||
})
|
||||
.returning({ id: users.id })
|
||||
|
||||
return { userId: newUser[0].id }
|
||||
} catch (error) {
|
||||
throw new Error('Error while creating user')
|
||||
}
|
||||
}
|
||||
|
||||
export const checkUserExists = async ({ email, strategy }: UserExistArgs) => {
|
||||
const userExists = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
with: {
|
||||
oauthTokens: {
|
||||
where: eq(oauthTokens.strategy, strategy),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return userExists
|
||||
}
|
||||
|
||||
export const createSession = async ({ userId }: NewSessionArgs) => {
|
||||
if (!userId) {
|
||||
throw new Error('User ID is required')
|
||||
}
|
||||
try {
|
||||
const newSession = await db
|
||||
.insert(sessions)
|
||||
.values({
|
||||
userId,
|
||||
expiresAt: expiresAt.getTime(),
|
||||
})
|
||||
.returning({ id: sessions.id })
|
||||
|
||||
return { sessionId: newSession[0].id, expiresAt }
|
||||
} catch (error) {
|
||||
throw new Error('Failed to create session')
|
||||
}
|
||||
}
|
||||
|
||||
export const saveOauthToken = async ({ accessToken, refreshToken, strategy, userId }: TokenArgs) => {
|
||||
try {
|
||||
await db.insert(oauthTokens).values({
|
||||
userId,
|
||||
strategy: 'google',
|
||||
accessToken,
|
||||
refreshToken,
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error('Error while creating token')
|
||||
}
|
||||
}
|
||||
|
||||
export const updateOauthToken = async ({ accessToken, refreshToken, strategy, userId }: TokenArgs) => {
|
||||
try {
|
||||
await db
|
||||
.update(oauthTokens)
|
||||
.set({
|
||||
accessToken,
|
||||
refreshToken,
|
||||
})
|
||||
.where(and(eq(oauthTokens.userId, userId), eq(oauthTokens.strategy, strategy)))
|
||||
} catch (error) {
|
||||
throw new Error('Error while creating token')
|
||||
}
|
||||
}
|
||||
|
||||
export const createLoginLog = async ({ userAgent, userId, sessionId, ip }: NewLogsArgs) => {
|
||||
if (!userAgent) {
|
||||
throw new Error('Internal Error')
|
||||
}
|
||||
const parser = Bowser.getParser(userAgent)
|
||||
|
||||
try {
|
||||
await db.insert(loginLogs).values({
|
||||
userId,
|
||||
sessionId,
|
||||
ip,
|
||||
os: `${parser.getOSName()} ${parser.getOSVersion()}`,
|
||||
browser: `${parser.getBrowserName()} ${parser.getBrowserVersion()}`,
|
||||
device: parser.getPlatformType(),
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error('Failed to create logs')
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import { and, eq, gte } from 'drizzle-orm'
|
||||
import { db } from '@/db'
|
||||
import { sessions } from '@/db/schema'
|
||||
|
||||
async function getUser(authToken: string | undefined) {
|
||||
if (!authToken) return null
|
||||
|
||||
const userInfo = await db.query.sessions.findFirst({
|
||||
where: and(eq(sessions.id, authToken), gte(sessions.expiresAt, new Date().getTime())),
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
userName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!userInfo) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!userInfo.user) {
|
||||
return null
|
||||
}
|
||||
return userInfo
|
||||
}
|
||||
|
||||
export default getUser
|
||||
@ -0,0 +1,10 @@
|
||||
import { defineMiddleware } from 'astro/middleware'
|
||||
import getUser from './lib/getUser'
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const userInfo = await getUser(context.cookies.get('app_auth_token')?.value)
|
||||
|
||||
context.locals.userId = userInfo?.user?.id
|
||||
|
||||
return next()
|
||||
})
|
||||
@ -0,0 +1,177 @@
|
||||
import type { APIContext } from 'astro'
|
||||
|
||||
import {
|
||||
checkUserExists,
|
||||
createLoginLog,
|
||||
createSession,
|
||||
createUser,
|
||||
saveOauthToken,
|
||||
updateOauthToken,
|
||||
} from '../../../../lib/auth'
|
||||
|
||||
export async function GET({ request, clientAddress, cookies }: APIContext) {
|
||||
const code = new URL(request.url).searchParams?.get('code')
|
||||
const state = new URL(request.url).searchParams?.get('state')
|
||||
|
||||
const storedState = cookies.get('google_oauth_state')?.value
|
||||
const codeVerifier = cookies.get('google_code_challenge')?.value
|
||||
|
||||
if (storedState !== state || !codeVerifier || !code) {
|
||||
cookies.delete('google_oauth_state', { path: '/' })
|
||||
cookies.delete('google_code_challenge', { path: '/' })
|
||||
|
||||
console.log('state mismatch')
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/login?error=Server+Error',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenUrl = 'https://www.googleapis.com/oauth2/v4/token'
|
||||
|
||||
const formData = new URLSearchParams()
|
||||
formData.append('grant_type', 'authorization_code')
|
||||
formData.append('client_id', import.meta.env.OAUTH_GOOGLE_CLIENT_ID)
|
||||
formData.append('client_secret', import.meta.env.OAUTH_GOOGLE_CLIENT_SECRET)
|
||||
formData.append('redirect_uri', import.meta.env.OAUTH_GOOGLE_CALLBACK_URL)
|
||||
formData.append('code', code)
|
||||
formData.append('code_verifier', codeVerifier)
|
||||
|
||||
console.log('fetching token', formData)
|
||||
|
||||
const fetchToken = await fetch(tokenUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
const fetchTokenRes = await fetchToken.json()
|
||||
|
||||
console.log('fetchTokenRes', fetchTokenRes)
|
||||
|
||||
const fetchUser = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
||||
headers: { Authorization: `Bearer ${fetchTokenRes.access_token}` },
|
||||
})
|
||||
|
||||
const fetchUserRes = await fetchUser.json()
|
||||
|
||||
console.log('fetchUserRes', fetchUserRes)
|
||||
|
||||
const userExists = await checkUserExists({
|
||||
email: fetchUserRes.email,
|
||||
strategy: 'google',
|
||||
})
|
||||
|
||||
console.log('userExists', userExists)
|
||||
|
||||
if (!userExists) {
|
||||
const { userId } = await createUser({
|
||||
email: fetchUserRes.email,
|
||||
fullName: fetchUserRes.name,
|
||||
profilePhoto: fetchUserRes.picture,
|
||||
userName: fetchUserRes.email.split('@')[0],
|
||||
})
|
||||
|
||||
await saveOauthToken({
|
||||
userId: userId,
|
||||
strategy: 'google',
|
||||
accessToken: fetchTokenRes.access_token,
|
||||
refreshToken: fetchTokenRes.refresh_token,
|
||||
})
|
||||
|
||||
const { sessionId } = await createSession({
|
||||
userId: userId,
|
||||
})
|
||||
|
||||
// log
|
||||
await createLoginLog({
|
||||
sessionId,
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
userId: userId,
|
||||
ip: clientAddress ?? 'dev',
|
||||
})
|
||||
|
||||
cookies.delete('google_oauth_state', { path: '/' })
|
||||
cookies.delete('google_code_challenge', { path: '/' })
|
||||
|
||||
cookies.set('app_auth_token', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: import.meta.env.PROD,
|
||||
})
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/profile',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
if (userExists.oauthTokens.length > 0) {
|
||||
// oauth strategy exists
|
||||
// update token
|
||||
|
||||
await updateOauthToken({
|
||||
userId: userExists.id,
|
||||
strategy: 'google',
|
||||
accessToken: fetchTokenRes.access_token,
|
||||
refreshToken: fetchTokenRes.refresh_token,
|
||||
})
|
||||
} else {
|
||||
await saveOauthToken({
|
||||
userId: userExists.id,
|
||||
strategy: 'google',
|
||||
accessToken: fetchTokenRes.access_token,
|
||||
refreshToken: fetchTokenRes.refresh_token,
|
||||
})
|
||||
}
|
||||
|
||||
const { sessionId } = await createSession({
|
||||
userId: userExists.id,
|
||||
})
|
||||
|
||||
await createLoginLog({
|
||||
sessionId,
|
||||
userAgent: request.headers.get('user-agent'),
|
||||
userId: userExists.id,
|
||||
ip: clientAddress ?? 'dev',
|
||||
})
|
||||
|
||||
cookies.delete('google_oauth_state', { path: '/' })
|
||||
cookies.delete('google_code_challenge', { path: '/' })
|
||||
|
||||
cookies.set('app_auth_token', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: import.meta.env.PROD,
|
||||
})
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
cookies.delete('google_oauth_state', { path: '/' })
|
||||
cookies.delete('google_code_challenge', { path: '/' })
|
||||
|
||||
console.error(error)
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/login?error=Server+Error',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import type { APIContext } from 'astro'
|
||||
import { init, createId } from '@paralleldrive/cuid2'
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
export async function GET({ cookies }: APIContext) {
|
||||
const generateId = init({ length: 40 })
|
||||
|
||||
const googleOauthState = createId()
|
||||
|
||||
cookies.set('google_oauth_state', googleOauthState, {
|
||||
path: '/',
|
||||
})
|
||||
|
||||
const googleCodeChallenge = generateId()
|
||||
const codeChallenge = createHash('sha256').update(googleCodeChallenge).digest('base64url')
|
||||
|
||||
cookies.set('google_code_challenge', googleCodeChallenge, {
|
||||
path: '/',
|
||||
})
|
||||
|
||||
const authorizationUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth')
|
||||
authorizationUrl.search = new URLSearchParams({
|
||||
access_type: 'offline',
|
||||
scope: 'openid email profile',
|
||||
prompt: 'consent',
|
||||
response_type: 'code',
|
||||
client_id: import.meta.env.OAUTH_GOOGLE_CLIENT_ID,
|
||||
redirect_uri: import.meta.env.OAUTH_GOOGLE_CALLBACK_URL,
|
||||
state: googleOauthState,
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
}).toString()
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: authorizationUrl.toString(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import type { APIContext } from 'astro'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import { db } from '@/db'
|
||||
import { sessions } from '@/db/schema'
|
||||
|
||||
export async function GET({ cookies }: APIContext) {
|
||||
const sessionId = cookies.get('app_auth_token')?.value
|
||||
if (!sessionId) {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
})
|
||||
}
|
||||
await db.delete(sessions).where(eq(sessions.id, sessionId))
|
||||
|
||||
cookies.delete('app_auth_token', { path: '/' })
|
||||
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import type { APIContext } from 'astro'
|
||||
import { and, eq, gte } from 'drizzle-orm'
|
||||
import { db } from '../../db'
|
||||
import { sessions, users } from '../../db/schema'
|
||||
|
||||
export async function POST({ request, cookies }: APIContext) {
|
||||
const requestBody = await request.formData()
|
||||
|
||||
const fullName = requestBody.get('fullName')
|
||||
const userName = requestBody.get('userName')
|
||||
try {
|
||||
const authToken = cookies.get('app_auth_token')?.value
|
||||
|
||||
if (!authToken) {
|
||||
return Response.json(
|
||||
{ error: 'authentication_error', message: 'Log in' },
|
||||
{
|
||||
status: 401,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const sessionInfo = await db.query.sessions.findFirst({
|
||||
where: and(eq(sessions.id, authToken), gte(sessions.expiresAt, new Date().getTime())),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!sessionInfo || !sessionInfo.user) {
|
||||
return Response.json(
|
||||
{ error: 'authorization_error', message: 'Log in' },
|
||||
{
|
||||
status: 403,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
fullName: fullName as string,
|
||||
userName: userName as string,
|
||||
})
|
||||
.where(eq(users.id, sessionInfo.user.id))
|
||||
|
||||
return Response.json(
|
||||
{ success: true, message: 'Profile Updated Sucessfully' },
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.log('error while creating profile', error)
|
||||
|
||||
return Response.json(
|
||||
{
|
||||
error: 'server_error',
|
||||
message: 'Internal server error. Try again later',
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
---
|
||||
const userIsLoggedIn = !!Astro.locals.userId
|
||||
|
||||
if (!userIsLoggedIn) {
|
||||
return Astro.redirect('/login')
|
||||
}
|
||||
---
|
||||
|
||||
<h1>Dashboard</h1>
|
||||
@ -0,0 +1,20 @@
|
||||
---
|
||||
import { sessions } from '../db/schema'
|
||||
import { db } from '../db'
|
||||
import { and, eq, gte } from 'drizzle-orm'
|
||||
|
||||
const authToken = Astro.cookies.get('app_auth_token')?.value
|
||||
|
||||
if (!authToken) {
|
||||
return Astro.redirect('/login')
|
||||
}
|
||||
|
||||
const userInfo = await db.query.sessions.findFirst({
|
||||
where: and(eq(sessions.id, authToken), gte(sessions.expiresAt, new Date().getTime())),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('auth', userInfo)
|
||||
---
|
||||
@ -0,0 +1,35 @@
|
||||
---
|
||||
import { eq, desc } from 'drizzle-orm'
|
||||
|
||||
import { db } from '@/db'
|
||||
import { loginLogs, sessions } from '@/db/schema'
|
||||
|
||||
const sessionToken = Astro.cookies.get('app_auth_token')?.value
|
||||
|
||||
if (!sessionToken) {
|
||||
return Astro.redirect('/')
|
||||
}
|
||||
|
||||
const userInfo = await db.query.sessions.findFirst({
|
||||
where: eq(sessions.id, sessionToken),
|
||||
with: {
|
||||
user: {
|
||||
with: {
|
||||
oauthTokens: {
|
||||
columns: {
|
||||
strategy: true,
|
||||
},
|
||||
},
|
||||
loginLogs: {
|
||||
orderBy: desc(loginLogs.loggedInAt),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const logs = userInfo?.user?.loginLogs.sort((a, b) => (a.sessionId === sessionToken ? -1 : 1))
|
||||
|
||||
console.log(userInfo)
|
||||
console.log(logs)
|
||||
---
|
||||
Reference in New Issue