-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from aritra1999/feature/api
feat: api
- Loading branch information
Showing
24 changed files
with
4,541 additions
and
1,246 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { Hono } from 'hono'; | ||
import { logger } from 'hono/logger'; | ||
import { initAuthConfig, type AuthConfig } from '@hono/auth-js'; | ||
import GitHub from '@auth/sveltekit/providers/github'; | ||
import { | ||
SECRET_AUTH_GITHUB_ID, | ||
SECRET_AUTH_GITHUB_SECRET, | ||
SECRET_AUTH_SECRET | ||
} from '$env/static/private'; | ||
import { userRouter } from './user/user.controller'; | ||
import { projectsRouter } from './projects/projects.controller'; | ||
import { websitesRouter } from './websites/websites.controller'; | ||
import { statusRouter } from './status/status.controller'; | ||
|
||
export const api = new Hono().basePath('/api'); | ||
|
||
api.use(initAuthConfig(getAuthConfig)); | ||
api.use(logger()); | ||
|
||
api.route('/user', userRouter); | ||
api.route('/projects', projectsRouter); | ||
api.route('/websites', websitesRouter); | ||
api.route('/status', statusRouter); | ||
|
||
function getAuthConfig(): AuthConfig { | ||
return { | ||
secret: SECRET_AUTH_SECRET, | ||
providers: [ | ||
GitHub({ | ||
clientId: SECRET_AUTH_GITHUB_ID, | ||
clientSecret: SECRET_AUTH_GITHUB_SECRET | ||
}) | ||
] | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import type { Context, Next } from 'hono'; | ||
import { z } from 'zod'; | ||
|
||
export const validateRequestBody = <T extends z.ZodType>(schema: T) => { | ||
return async (context: Context, next: Next) => { | ||
try { | ||
const body = await context.req.json(); | ||
const valid = schema.safeParse(body); | ||
|
||
if (valid.error) { | ||
const errorResponse = { | ||
error: 'Invalid input', | ||
details: valid.error.errors.map((issue) => `${issue.path.join('.')} is ${issue.message}`) | ||
}; | ||
return context.json(errorResponse, 400); | ||
} | ||
|
||
context.set('requestBody', valid.data); | ||
await next(); | ||
} catch (error) { | ||
console.log(error); | ||
return context.json({ error: 'Invalid input json' }, 400); | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Hono, type Context } from 'hono'; | ||
import { verifyAuth } from '@hono/auth-js'; | ||
import { | ||
createProject, | ||
deleteProject, | ||
getAllProjects, | ||
getProjectBySlug, | ||
updateProject | ||
} from './projects.service'; | ||
import { InsertProjectSchema } from '$lib/db/schema'; | ||
import { validateRequestBody } from '../middlewares'; | ||
|
||
export const projectsRouter = new Hono(); | ||
|
||
export const getProjectsController = async (context: Context) => { | ||
const { token } = context.get('authUser'); | ||
if (!token) return context.status(401); | ||
|
||
const projects = await getAllProjects(String(token.id)); | ||
|
||
return context.json({ projects }); | ||
}; | ||
|
||
export const getProjectController = async (context: Context) => { | ||
const { token } = context.get('authUser'); | ||
if (!token) return context.status(401); | ||
|
||
const { slug } = context.req.param(); | ||
if (!slug) return context.json({ error: 'Invalid slug' }, 400); | ||
|
||
const project = await getProjectBySlug(String(token.id), slug); | ||
if (!project) return context.json({ error: 'Project not found' }, 404); | ||
|
||
return context.json(project); | ||
}; | ||
|
||
export const postProjectController = async (context: Context) => { | ||
const { token } = context.get('authUser'); | ||
if (!token) return context.status(401); | ||
|
||
const requestBody = context.get('requestBody'); | ||
|
||
return await createProject({ | ||
userId: token.id as string, | ||
name: requestBody.name as string, | ||
description: requestBody.description as string, | ||
slug: requestBody.slug as string | ||
}) | ||
.then((response) => { | ||
if (response.error) { | ||
return context.json({ error: response.error }, response.status); | ||
} | ||
|
||
return context.json(response.data, response.status); | ||
}) | ||
.catch((error) => { | ||
return context.json({ error }, 400); | ||
}); | ||
}; | ||
|
||
export const putProjectController = async (context: Context) => { | ||
const { token } = context.get('authUser'); | ||
if (!token) return context.status(401); | ||
|
||
const { slug } = context.req.param(); | ||
if (!slug) return context.json({ error: 'Invalid slug' }, 400); | ||
|
||
const requestBody = context.get('requestBody'); | ||
const updatedProject = await updateProject(String(token.id), slug, requestBody); | ||
|
||
return context.json( | ||
updatedProject.data ?? { error: updatedProject.error }, | ||
updatedProject.status | ||
); | ||
}; | ||
|
||
export const deleteProjectController = async (context: Context) => { | ||
const { token } = context.get('authUser'); | ||
if (!token) return context.status(401); | ||
|
||
const { slug } = context.req.param(); | ||
if (!slug) return context.json({ error: 'Invalid slug' }, 400); | ||
|
||
const deletedProject = await deleteProject(String(token.id), slug); | ||
return context.json( | ||
deletedProject.data ?? { error: deletedProject.error }, | ||
deletedProject.status | ||
); | ||
}; | ||
|
||
const PartialInsertProjectSchema = InsertProjectSchema.pick({ | ||
name: true, | ||
description: true, | ||
slug: true | ||
}); | ||
|
||
projectsRouter.use(verifyAuth()); | ||
projectsRouter.get('/:slug', getProjectController); | ||
projectsRouter.get('/', getProjectsController); | ||
projectsRouter.post('/', validateRequestBody(PartialInsertProjectSchema), postProjectController); | ||
projectsRouter.put('/:slug', validateRequestBody(PartialInsertProjectSchema), putProjectController); | ||
projectsRouter.delete('/:slug', deleteProjectController); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import { db } from '$lib/db/drizzle'; | ||
import { and, count, eq } from 'drizzle-orm'; | ||
import { projects, type InsertProject, type SelectProjectPartial } from '$lib/db/schema'; | ||
import { isUserPro } from '../user/user.service'; | ||
import type { ServiceResponse } from '../types'; | ||
import { prettifyErrors } from '$lib/db/utils'; | ||
import type { StatusCode } from 'hono/utils/http-status'; | ||
|
||
export const canUserCreateProject = async (userId: string): Promise<boolean> => { | ||
if (await isUserPro(userId)) return true; | ||
|
||
const projectsCount = await db | ||
.select({ count: count(projects.id) }) | ||
.from(projects) | ||
.where(eq(projects.userId, userId)); | ||
|
||
return projectsCount[0].count < 2; | ||
}; | ||
|
||
export const getAllProjects = async (userId: string): Promise<SelectProjectPartial[]> => { | ||
return db | ||
.select({ | ||
id: projects.id, | ||
name: projects.name, | ||
description: projects.description, | ||
slug: projects.slug | ||
}) | ||
.from(projects) | ||
.where(eq(projects.userId, userId)); | ||
}; | ||
|
||
export const getProjectBySlug = async ( | ||
userId: string, | ||
projectSlug: string | ||
): Promise<SelectProjectPartial | null> => { | ||
const projectResponse = await db | ||
.select({ | ||
id: projects.id, | ||
name: projects.name, | ||
description: projects.description, | ||
slug: projects.slug | ||
}) | ||
.from(projects) | ||
.where(and(eq(projects.userId, userId), eq(projects.slug, projectSlug))) | ||
.limit(1); | ||
|
||
return projectResponse[0]; | ||
}; | ||
|
||
export const createProject = async ( | ||
project: InsertProject | ||
): Promise<ServiceResponse<SelectProjectPartial>> => { | ||
if (!(await canUserCreateProject(project.userId))) { | ||
return { | ||
status: 403, | ||
error: 'Hobby users can create 2 projects!' | ||
}; | ||
} | ||
|
||
try { | ||
const result = await db.insert(projects).values(project).returning({ | ||
id: projects.id, | ||
name: projects.name, | ||
description: projects.description, | ||
slug: projects.slug | ||
}); | ||
return { | ||
status: 200, | ||
data: result[0] | ||
}; | ||
} catch (err) { | ||
return { | ||
status: 400, | ||
error: prettifyErrors(err as Error) | ||
}; | ||
} | ||
}; | ||
|
||
export const updateProject = async ( | ||
userId: string, | ||
slug: string, | ||
updatedProjectInput: InsertProject | ||
): Promise<ServiceResponse<SelectProjectPartial>> => { | ||
const project = await getProjectBySlug(userId, slug); | ||
|
||
if (!project) | ||
return { | ||
status: 404, | ||
error: 'Project not found' | ||
}; | ||
|
||
return await db | ||
.update(projects) | ||
.set(updatedProjectInput) | ||
.where(and(eq(projects.userId, userId), eq(projects.slug, slug))) | ||
.returning({ | ||
id: projects.id, | ||
name: projects.name, | ||
description: projects.description, | ||
slug: projects.slug | ||
}) | ||
.then((response) => { | ||
return { | ||
status: 200 as StatusCode, | ||
data: response[0] | ||
}; | ||
}) | ||
.catch((error) => { | ||
return { | ||
status: 400 as StatusCode, | ||
error: error.message | ||
}; | ||
}); | ||
}; | ||
|
||
export const deleteProject = async ( | ||
userId: string, | ||
slug: string | ||
): Promise<ServiceResponse<{ deletedId: string }>> => { | ||
return await db | ||
.delete(projects) | ||
.where(and(eq(projects.userId, userId), eq(projects.slug, slug))) | ||
.returning({ deletedId: projects.id }) | ||
.then((response) => { | ||
if (response.length === 0) | ||
return { | ||
status: 404 as StatusCode, | ||
error: 'Project not found' | ||
}; | ||
return { | ||
status: 200 as StatusCode, | ||
data: response[0] | ||
}; | ||
}) | ||
.catch((error) => { | ||
return { | ||
status: 400 as StatusCode, | ||
error: error.message | ||
}; | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { Hono, type Context } from 'hono'; | ||
import { getStatus, getStatusByProject, getStatusPage } from './status.service'; | ||
|
||
export const statusRouter = new Hono(); | ||
|
||
export const getStatusController = async (context: Context) => { | ||
const { websiteId } = context.req.param(); | ||
if (!websiteId) return context.json({ error: 'Missing website ID' }, 400); | ||
|
||
const websiteResponse = await getStatus(websiteId); | ||
|
||
return context.json( | ||
websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data, | ||
websiteResponse.status | ||
); | ||
}; | ||
|
||
export const getStatusByProjectController = async (context: Context) => { | ||
const { projectSlug } = context.req.param(); | ||
if (!projectSlug) return context.json({ error: 'Missing project slug' }, 400); | ||
|
||
const websiteResponse = await getStatusByProject(projectSlug); | ||
|
||
return context.json( | ||
websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data, | ||
websiteResponse.status | ||
); | ||
}; | ||
|
||
export const getStatusPageController = async (context: Context) => { | ||
const { projectId } = context.req.param(); | ||
if (!projectId) return context.json({ error: 'Missing project ID' }, 400); | ||
|
||
const websiteResponse = await getStatusPage(projectId); | ||
|
||
return context.json( | ||
websiteResponse.error ? { error: websiteResponse.error } : websiteResponse.data, | ||
websiteResponse.status | ||
); | ||
}; | ||
|
||
statusRouter.get('/page/:projectId', getStatusPageController); | ||
statusRouter.get('/project/:projectSlug', getStatusByProjectController); | ||
statusRouter.get('/:websiteId', getStatusController); |
Oops, something went wrong.