Skip to content

Commit 9d95f71

Browse files
committed
Initialize project with basic setting
0 parents  commit 9d95f71

490 files changed

Lines changed: 20212 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/deploy.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Deploy to Lightsail
2+
3+
on:
4+
push:
5+
branches:
6+
- prod
7+
8+
jobs:
9+
deploy:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Bun
17+
uses: oven-sh/setup-bun@v2
18+
19+
- name: Install dependencies
20+
run: bun install --frozen-lockfile
21+
22+
- name: Create .env file
23+
run: |
24+
echo "${{ secrets.ENV_TEXT }}" > .env
25+
cp .env apps/web/.env
26+
cp .env apps/api/.env
27+
28+
- name: Typecheck API
29+
run: bun run typecheck
30+
31+
- name: Build web app
32+
run: bun run build
33+
34+
- name: Build Docker image
35+
run: docker build -t calendar-app:latest .
36+
37+
- name: Save Docker image
38+
run: docker save calendar-app:latest | gzip > calendar-app.tar.gz
39+
40+
- name: Create target directory
41+
uses: appleboy/[email protected]
42+
with:
43+
host: ${{ secrets.HOST }}
44+
username: ${{ secrets.USERNAME }}
45+
key: ${{ secrets.SSH_KEY }}
46+
script: mkdir -p ~/calendar-app
47+
48+
- name: Copy files to server
49+
uses: appleboy/[email protected]
50+
with:
51+
host: ${{ secrets.HOST }}
52+
username: ${{ secrets.USERNAME }}
53+
key: ${{ secrets.SSH_KEY }}
54+
source: calendar-app.tar.gz,compose.yml,.env
55+
target: ~/calendar-app
56+
timeout: 600s
57+
58+
- name: Deploy
59+
uses: appleboy/[email protected]
60+
with:
61+
host: ${{ secrets.HOST }}
62+
username: ${{ secrets.USERNAME }}
63+
key: ${{ secrets.SSH_KEY }}
64+
script: |
65+
cd ~/calendar-app
66+
docker load < calendar-app.tar.gz
67+
docker compose down || true
68+
docker compose up -d
69+
rm calendar-app.tar.gz

.gitignore

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
node_modules
2+
3+
# Output
4+
.output
5+
.vercel
6+
.netlify
7+
.wrangler
8+
.svelte-kit
9+
build
10+
11+
# OS
12+
.DS_Store
13+
Thumbs.db
14+
15+
# Env
16+
.env
17+
.env.*
18+
!.env.example
19+
!.env.test
20+
21+
# Vite
22+
vite.config.js.timestamp-*
23+
vite.config.ts.timestamp-*
24+
25+
# Paraglide
26+
apps/web/src/lib/paraglide
27+
apps/web/project.inlang/cache/
28+
29+
# Bun
30+
bun.lockb
31+
32+
# Docker
33+
*.tar.gz

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
FROM node:22-slim
2+
WORKDIR /app
3+
4+
ENV NODE_ENV=production
5+
ENV PORT=3000
6+
ENV API_PORT=4000
7+
8+
RUN npm install -g bun
9+
10+
COPY apps/web/build ./apps/web/build
11+
COPY apps/web/package.json ./apps/web/
12+
13+
COPY apps/api ./apps/api
14+
15+
COPY package.json bun.lock ./
16+
RUN bun install --frozen-lockfile --production
17+
18+
COPY start.sh ./
19+
RUN chmod +x start.sh
20+
21+
EXPOSE 3000 4000
22+
23+
CMD ["./start.sh"]

apps/api/auth.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { betterAuth } from 'better-auth'
2+
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
3+
import { db } from '@db/index'
4+
import * as schema from '@db/schema'
5+
6+
const BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET
7+
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID
8+
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET
9+
10+
if (!BETTER_AUTH_SECRET) throw new Error('BETTER_AUTH_SECRET is not set')
11+
if (!GOOGLE_CLIENT_ID) throw new Error('GOOGLE_CLIENT_ID is not set')
12+
if (!GOOGLE_CLIENT_SECRET) throw new Error('GOOGLE_CLIENT_SECRET is not set')
13+
14+
const BETTER_AUTH_URL = process.env.BETTER_AUTH_URL
15+
const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000'
16+
17+
if (!BETTER_AUTH_URL) throw new Error('BETTER_AUTH_URL is not set')
18+
19+
export const auth = betterAuth({
20+
baseURL: BETTER_AUTH_URL,
21+
secret: BETTER_AUTH_SECRET,
22+
database: drizzleAdapter(db, {
23+
provider: 'mysql',
24+
schema,
25+
}),
26+
trustedOrigins: [FRONTEND_URL, BETTER_AUTH_URL],
27+
emailAndPassword: {
28+
enabled: true,
29+
},
30+
socialProviders: {
31+
google: {
32+
clientId: GOOGLE_CLIENT_ID,
33+
clientSecret: GOOGLE_CLIENT_SECRET,
34+
},
35+
},
36+
})
37+
38+
export type AuthSession = typeof auth.$Infer.Session

apps/api/db/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { drizzle } from 'drizzle-orm/mysql2'
2+
import mysql from 'mysql2/promise'
3+
import * as schema from './schema'
4+
5+
export type Database = ReturnType<typeof createDb>
6+
7+
export const createDb = (pool: mysql.Pool) => drizzle(pool, { schema, mode: 'default' })
8+
9+
export const createPool = (url: string) =>
10+
mysql.createPool({
11+
uri: url,
12+
timezone: '+00:00',
13+
})
14+
15+
const poolConnection = mysql.createPool({
16+
host: process.env.DATABASE_HOST,
17+
port: parseInt(process.env.DATABASE_PORT!),
18+
user: process.env.DATABASE_USERNAME,
19+
database: process.env.DATABASE_NAME,
20+
password: process.env.DATABASE_PASSWORD,
21+
})
22+
23+
export const db = createDb(poolConnection)

apps/api/db/schema.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { mysqlTable, varchar, text, boolean, timestamp, datetime, tinyint, json, mysqlEnum } from 'drizzle-orm/mysql-core'
2+
3+
export const user = mysqlTable('user', {
4+
id: varchar('id', { length: 36 }).primaryKey(),
5+
name: varchar('name', { length: 255 }).notNull(),
6+
email: varchar('email', { length: 255 }).notNull().unique(),
7+
emailVerified: boolean('email_verified').notNull().default(false),
8+
image: text('image'),
9+
timezone: varchar('timezone', { length: 64 }).notNull().default('Asia/Seoul'),
10+
createdAt: timestamp('created_at').notNull().defaultNow(),
11+
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
12+
})
13+
14+
export const session = mysqlTable('session', {
15+
id: varchar('id', { length: 36 }).primaryKey(),
16+
userId: varchar('user_id', { length: 36 })
17+
.notNull()
18+
.references(() => user.id, { onDelete: 'cascade' }),
19+
token: varchar('token', { length: 255 }).notNull().unique(),
20+
expiresAt: timestamp('expires_at').notNull(),
21+
ipAddress: varchar('ip_address', { length: 45 }),
22+
userAgent: text('user_agent'),
23+
createdAt: timestamp('created_at').notNull().defaultNow(),
24+
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
25+
})
26+
27+
export const account = mysqlTable('account', {
28+
id: varchar('id', { length: 36 }).primaryKey(),
29+
userId: varchar('user_id', { length: 36 })
30+
.notNull()
31+
.references(() => user.id, { onDelete: 'cascade' }),
32+
accountId: varchar('account_id', { length: 255 }).notNull(),
33+
providerId: varchar('provider_id', { length: 255 }).notNull(),
34+
accessToken: text('access_token'),
35+
refreshToken: text('refresh_token'),
36+
accessTokenExpiresAt: timestamp('access_token_expires_at'),
37+
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
38+
scope: text('scope'),
39+
idToken: text('id_token'),
40+
password: text('password'),
41+
createdAt: timestamp('created_at').notNull().defaultNow(),
42+
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
43+
})
44+
45+
export const verification = mysqlTable('verification', {
46+
id: varchar('id', { length: 36 }).primaryKey(),
47+
identifier: varchar('identifier', { length: 255 }).notNull(),
48+
value: text('value').notNull(),
49+
expiresAt: timestamp('expires_at').notNull(),
50+
createdAt: timestamp('created_at').notNull().defaultNow(),
51+
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
52+
})
53+
54+
export type RRuleType = {
55+
freq: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY'
56+
interval?: number
57+
count?: number
58+
until?: string
59+
byDay?: string[]
60+
byMonth?: number[]
61+
byMonthDay?: number[]
62+
} | null
63+
64+
export const calendarEvent = mysqlTable('calendar_event', {
65+
id: varchar('id', { length: 36 }).primaryKey(),
66+
userId: varchar('user_id', { length: 36 })
67+
.notNull()
68+
.references(() => user.id, { onDelete: 'cascade' }),
69+
uid: varchar('uid', { length: 255 }).notNull().unique(),
70+
summary: varchar('summary', { length: 500 }).notNull(),
71+
description: text('description'),
72+
location: varchar('location', { length: 500 }),
73+
dtstart: datetime('dtstart').notNull(),
74+
dtend: datetime('dtend').notNull(),
75+
isAllDay: boolean('is_all_day').notNull().default(false),
76+
rrule: json('rrule').$type<RRuleType>(),
77+
exdate: json('exdate').$type<string[]>(),
78+
status: mysqlEnum('status', ['TENTATIVE', 'CONFIRMED', 'CANCELLED']).default('CONFIRMED'),
79+
transp: mysqlEnum('transp', ['TRANSPARENT', 'OPAQUE']).default('OPAQUE'),
80+
priority: tinyint('priority'),
81+
categories: json('categories').$type<string[]>(),
82+
color: varchar('color', { length: 50 }),
83+
sequence: tinyint('sequence').notNull().default(0),
84+
dtstamp: datetime('dtstamp').notNull(),
85+
createdAt: timestamp('created_at').notNull().defaultNow(),
86+
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
87+
})
88+
89+
export const deletedCalendarEvent = mysqlTable('deleted_calendar_event', {
90+
id: varchar('id', { length: 36 }).primaryKey(),
91+
userId: varchar('user_id', { length: 36 })
92+
.notNull()
93+
.references(() => user.id, { onDelete: 'cascade' }),
94+
uid: varchar('uid', { length: 255 }).notNull(),
95+
deletedAt: timestamp('deleted_at').notNull().defaultNow(),
96+
syncToken: varchar('sync_token', { length: 64 }).notNull(),
97+
})
98+
99+
export const calendarSubscription = mysqlTable('calendar_subscription', {
100+
id: varchar('id', { length: 36 }).primaryKey(),
101+
userId: varchar('user_id', { length: 36 })
102+
.notNull()
103+
.references(() => user.id, { onDelete: 'cascade' }),
104+
token: varchar('token', { length: 64 }).notNull().unique(),
105+
icsToken: varchar('ics_token', { length: 64 }).notNull().unique(),
106+
name: varchar('name', { length: 255 }),
107+
isActive: boolean('is_active').notNull().default(true),
108+
ctag: varchar('ctag', { length: 64 }).notNull().default('0'),
109+
lastAccessedAt: datetime('last_accessed_at'),
110+
createdAt: timestamp('created_at').notNull().defaultNow(),
111+
updatedAt: timestamp('updated_at').notNull().defaultNow().onUpdateNow(),
112+
})
113+
114+
export type User = typeof user.$inferSelect
115+
export type Session = typeof session.$inferSelect
116+
export type Account = typeof account.$inferSelect
117+
export type CalendarEventRow = typeof calendarEvent.$inferSelect
118+
export type CalendarSubscriptionRow = typeof calendarSubscription.$inferSelect

apps/api/drizzle.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineConfig } from 'drizzle-kit'
2+
3+
export default defineConfig({
4+
schema: './db/schema.ts',
5+
out: './drizzle',
6+
dialect: 'mysql',
7+
dbCredentials: {
8+
host: process.env.DATABASE_HOST!,
9+
port: parseInt(process.env.DATABASE_PORT!),
10+
database: process.env.DATABASE_NAME!,
11+
user: process.env.DATABASE_USERNAME!,
12+
password: process.env.DATABASE_PASSWORD!,
13+
},
14+
})

apps/api/middleware/auth.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createMiddleware } from 'hono/factory'
2+
import { auth } from '../auth'
3+
import type { CalendarService, CalendarSubscription } from '@service/calendar'
4+
import type { CaldavService } from '@service/caldav'
5+
import type { AuthSession } from '../auth'
6+
7+
export type AppEnv = {
8+
Variables: {
9+
session: AuthSession
10+
calendarService: CalendarService
11+
caldavService: CaldavService
12+
caldavSubscription: CalendarSubscription
13+
caldavUserId: string
14+
}
15+
}
16+
17+
export const authMiddleware = createMiddleware<AppEnv>(async (c, next) => {
18+
const session = await auth.api.getSession({ headers: c.req.raw.headers })
19+
if (!session?.user) {
20+
return c.json({ error: 'Unauthorized' }, 401)
21+
}
22+
c.set('session', session)
23+
await next()
24+
})

apps/api/middleware/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { OpenAPIHono } from '@hono/zod-openapi'
2+
import { cors } from 'hono/cors'
3+
import type { AppEnv } from './auth'
4+
import { authMiddleware } from './auth'
5+
import { createServiceMiddleware } from './service'
6+
import type { CalendarService } from '@service/calendar'
7+
import type { CaldavService } from '@service/caldav'
8+
9+
export type { AppEnv } from './auth'
10+
export { authMiddleware } from './auth'
11+
export { createServiceMiddleware } from './service'
12+
13+
type InitMiddlewareDeps = {
14+
calendarService: CalendarService
15+
caldavService: CaldavService
16+
}
17+
18+
export const initMiddleware = (app: OpenAPIHono<AppEnv>, deps: InitMiddlewareDeps) => {
19+
app.use(
20+
'/api/*',
21+
cors({
22+
origin: (origin) => origin,
23+
credentials: true,
24+
}),
25+
)
26+
app.use('*', createServiceMiddleware(deps.calendarService, deps.caldavService))
27+
28+
app.use('/api/events/*', authMiddleware)
29+
app.use('/api/events', authMiddleware)
30+
app.use('/api/calendar/subscription/*', authMiddleware)
31+
app.use('/api/calendar/subscription', authMiddleware)
32+
}

apps/api/middleware/service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createMiddleware } from 'hono/factory'
2+
import type { AppEnv } from './auth'
3+
import type { CalendarService } from '@service/calendar'
4+
import type { CaldavService } from '@service/caldav'
5+
6+
export const createServiceMiddleware = (calendarService: CalendarService, caldavService: CaldavService) =>
7+
createMiddleware<AppEnv>(async (c, next) => {
8+
c.set('calendarService', calendarService)
9+
c.set('caldavService', caldavService)
10+
await next()
11+
})

0 commit comments

Comments
 (0)