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" />
|
/// <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