feat: added drizzle and oauth support

main
Antonio De Lucreziis 2 years ago
parent 39f42ade64
commit 1717e24563

@ -19,7 +19,7 @@ export default defineConfig({
vite: {
server: {
proxy: {
'/api': {
'/api/container': {
target: 'http://localhost:5432',
ws: true,
rewriteWsOrigin: true,

@ -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
}
]
}

4406
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -15,6 +15,8 @@
"@fontsource-variable/inter": "^5.1.0",
"@fontsource-variable/material-symbols-outlined": "^5.1.0",
"@fontsource/jetbrains-mono": "^5.1.0",
"@libsql/client": "^0.14.0",
"@paralleldrive/cuid2": "^2.2.2",
"@preact/signals": "^1.3.0",
"@uiw/codemirror-theme-github": "^4.23.2",
"@uiw/react-codemirror": "^4.23.2",
@ -22,9 +24,12 @@
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"astro": "^4.15.6",
"bowser": "^2.11.0",
"clsx": "^2.1.1",
"codemirror": "^5.65.17",
"drizzle-orm": "^0.33.0",
"express": "^4.21.0",
"nanoid": "^5.0.7",
"node-pty": "^1.0.0",
"preact": "^10.24.0",
"signal-exit": "^4.1.0",
@ -34,6 +39,7 @@
"devDependencies": {
"@types/bun": "^1.1.9",
"@types/express": "^4.17.21",
"drizzle-kit": "^0.24.2",
"npm-run-all": "^4.1.5"
}
}

@ -655,7 +655,7 @@ a:visited {
place-content: center;
background: linear-gradient(to bottom right, #f5efff 0%, #fafcfe 100%);
background: linear-gradient(to bottom right, #efe6ff 0%, #fafcfe 100%);
> section {
display: grid;

@ -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],
}),
}))

6
src/env.d.ts vendored

@ -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>

@ -1,5 +1,7 @@
---
import Base from '@/layouts/Base.astro'
const userIsLoggedIn = !!Astro.locals.userId
---
<Base>
@ -9,7 +11,7 @@ import Base from '@/layouts/Base.astro'
</nav>
<div class="spacer"></div>
<nav>
<a href="/login">Login</a>
{userIsLoggedIn ? <a href="/dashboard">Dashboard</a> : <a href="/login">Login</a>}
</nav>
</header>
<div class="homepage">

@ -16,15 +16,14 @@ console.log('env:', Object.fromEntries(Object.entries(import.meta.env).filter(([
</header>
<div class="page">
<h1>Login</h1>
<form action="/login" method="post">
<!-- <form action="/login" method="post">
<label for="username">Username</label>
<input type="text" id="username" name="username" required />
<label for="password">Password</label>
<input type="password" id="password" name="password" required />
<button type="submit">Login</button>
</form>
</form> -->
<h2>Or login with</h2>
<a role="button" href="/auth/github">GitHub</a>
<a role="button" href="/auth/oauth-provider-1">OAuth Provider 1</a>
<a role="button" href="/api/auth/google">Google</a>
</div>
</Base>

@ -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)
---

@ -4,6 +4,7 @@
"jsx": "react-jsx",
"jsxImportSource": "preact",
"baseUrl": ".",
"strict": true,
"paths": {
"@/*": ["src/*"]
}