added tooling (eslint, typescript and prettier)

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-11-02 12:14:37 +01:00
parent 575b3c23bf
commit 493e1b7650
82 changed files with 1890 additions and 1363 deletions

View File

@@ -1,13 +1,14 @@
import { createTRPCRouter } from "@/server/api/trpc";
import { chartRouter } from "./routers/chart";
import { reportRouter } from "./routers/report";
import { organizationRouter } from "./routers/organization";
import { userRouter } from "./routers/user";
import { projectRouter } from "./routers/project";
import { clientRouter } from "./routers/client";
import { dashboardRouter } from "./routers/dashboard";
import { eventRouter } from "./routers/event";
import { profileRouter } from "./routers/profile";
import { createTRPCRouter } from '@/server/api/trpc';
import { chartRouter } from './routers/chart';
import { clientRouter } from './routers/client';
import { dashboardRouter } from './routers/dashboard';
import { eventRouter } from './routers/event';
import { organizationRouter } from './routers/organization';
import { profileRouter } from './routers/profile';
import { projectRouter } from './routers/project';
import { reportRouter } from './routers/report';
import { userRouter } from './routers/user';
/**
* This is the primary router for your server.

View File

@@ -1,15 +1,15 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { last, pipe, sort, uniq } from "ramda";
import { toDots } from "@/utils/object";
import { zChartInputWithDates } from "@/utils/validation";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import {
type IChartInputWithDates,
type IChartEvent,
type IChartInputWithDates,
type IChartRange,
} from "@/types";
import { getDaysOldDate } from "@/utils/date";
} from '@/types';
import { getDaysOldDate } from '@/utils/date';
import { toDots } from '@/utils/object';
import { zChartInputWithDates } from '@/utils/validation';
import { last, pipe, sort, uniq } from 'ramda';
import { z } from 'zod';
export const config = {
api: {
@@ -21,7 +21,7 @@ export const chartRouter = createTRPCRouter({
events: protectedProcedure.query(async () => {
const events = await db.event.findMany({
take: 500,
distinct: ["name"],
distinct: ['name'],
});
return events;
@@ -47,12 +47,12 @@ export const chartRouter = createTRPCRouter({
const dotNotation = toDots(properties);
return [...acc, ...Object.keys(dotNotation)];
}, [] as string[])
.map((item) => item.replace(/\.([0-9]+)\./g, ".*."))
.map((item) => item.replace(/\.([0-9]+)/g, "[*]"));
.map((item) => item.replace(/\.([0-9]+)\./g, '.*.'))
.map((item) => item.replace(/\.([0-9]+)/g, '[*]'));
return pipe(
sort<string>((a, b) => a.length - b.length),
uniq,
uniq
)(properties);
}),
@@ -62,10 +62,10 @@ export const chartRouter = createTRPCRouter({
if (isJsonPath(input.property)) {
const events = await db.$queryRawUnsafe<{ value: string }[]>(
`SELECT ${selectJsonPath(
input.property,
input.property
)} AS value from events WHERE name = '${
input.event
}' AND "createdAt" >= NOW() - INTERVAL '30 days'`,
}' AND "createdAt" >= NOW() - INTERVAL '30 days'`
);
return {
values: uniq(events.map((item) => item.value)),
@@ -102,12 +102,12 @@ export const chartRouter = createTRPCRouter({
...(await getChartData({
...input,
event,
})),
}))
);
}
const sorted = [...series].sort((a, b) => {
if (input.chartType === "linear") {
if (input.chartType === 'linear') {
const sumA = a.data.reduce((acc, item) => acc + item.count, 0);
const sumB = b.data.reduce((acc, item) => acc + item.count, 0);
return sumB - sumA;
@@ -132,8 +132,8 @@ export const chartRouter = createTRPCRouter({
}
return acc;
},
{} as Record<(typeof series)[number]["event"]["id"], number>,
),
{} as Record<(typeof series)[number]['event']['id'], number>
)
).map(([id, count]) => ({
count,
...events.find((event) => event.id === id)!,
@@ -148,13 +148,13 @@ export const chartRouter = createTRPCRouter({
function selectJsonPath(property: string) {
const jsonPath = property
.replace(/^properties\./, "")
.replace(/\.\*\./g, ".**.");
.replace(/^properties\./, '')
.replace(/\.\*\./g, '.**.');
return `jsonb_path_query(properties, '$.${jsonPath}')`;
}
function isJsonPath(property: string) {
return property.startsWith("properties");
return property.startsWith('properties');
}
type ResultItem = {
@@ -164,12 +164,12 @@ type ResultItem = {
};
function propertyNameToSql(name: string) {
if (name.includes(".")) {
if (name.includes('.')) {
const str = name
.split(".")
.split('.')
.map((item, index) => (index === 0 ? item : `'${item}'`))
.join("->");
const findLastOf = "->";
.join('->');
const findLastOf = '->';
const lastArrow = str.lastIndexOf(findLastOf);
if (lastArrow === -1) {
return str;
@@ -224,27 +224,34 @@ function getDatesFromRange(range: IChartRange) {
};
}
function getChartSql({ event, chartType, breakdowns, interval, startDate, endDate }: Omit<IGetChartDataInput, 'range'>) {
function getChartSql({
event,
chartType,
breakdowns,
interval,
startDate,
endDate,
}: Omit<IGetChartDataInput, 'range'>) {
const select = [];
const where = [];
const groupBy = [];
const orderBy = [];
if (event.segment === "event") {
if (event.segment === 'event') {
select.push(`count(*)::int as count`);
} else {
select.push(`count(DISTINCT profile_id)::int as count`);
}
switch (chartType) {
case "bar": {
orderBy.push("count DESC");
case 'bar': {
orderBy.push('count DESC');
break;
}
case "linear": {
case 'linear': {
select.push(`date_trunc('${interval}', "createdAt") as date`);
groupBy.push("date");
orderBy.push("date");
groupBy.push('date');
orderBy.push('date');
break;
}
}
@@ -256,38 +263,38 @@ function getChartSql({ event, chartType, breakdowns, interval, startDate, endDat
filters.forEach((filter) => {
const { name, value } = filter;
switch (filter.operator) {
case "is": {
if (name.includes(".*.") || name.endsWith("[*]")) {
case 'is': {
if (name.includes('.*.') || name.endsWith('[*]')) {
where.push(
`properties @? '$.${name
.replace(/^properties\./, "")
.replace(/\.\*\./g, "[*].")} ? (${value
.replace(/^properties\./, '')
.replace(/\.\*\./g, '[*].')} ? (${value
.map((val) => `@ == "${val}"`)
.join(" || ")})'`,
.join(' || ')})'`
);
} else {
where.push(
`${propertyNameToSql(name)} in (${value
.map((val) => `'${val}'`)
.join(", ")})`,
.join(', ')})`
);
}
break;
}
case "isNot": {
if (name.includes(".*.") || name.endsWith("[*]")) {
case 'isNot': {
if (name.includes('.*.') || name.endsWith('[*]')) {
where.push(
`properties @? '$.${name
.replace(/^properties\./, "")
.replace(/\.\*\./g, "[*].")} ? (${value
.replace(/^properties\./, '')
.replace(/\.\*\./g, '[*].')} ? (${value
.map((val) => `@ != "${val}"`)
.join(" && ")})'`,
.join(' && ')})'`
);
} else if (name.includes(".")) {
} else if (name.includes('.')) {
where.push(
`${propertyNameToSql(name)} not in (${value
.map((val) => `'${val}'`)
.join(", ")})`,
.join(', ')})`
);
}
break;
@@ -322,24 +329,24 @@ function getChartSql({ event, chartType, breakdowns, interval, startDate, endDat
}
const sql = [
`SELECT ${select.join(", ")}`,
`SELECT ${select.join(', ')}`,
`FROM events`,
`WHERE ${where.join(" AND ")}`,
`WHERE ${where.join(' AND ')}`,
];
if (groupBy.length) {
sql.push(`GROUP BY ${groupBy.join(", ")}`);
sql.push(`GROUP BY ${groupBy.join(', ')}`);
}
if (orderBy.length) {
sql.push(`ORDER BY ${orderBy.join(", ")}`);
sql.push(`ORDER BY ${orderBy.join(', ')}`);
}
return sql.join("\n");
return sql.join('\n');
}
type IGetChartDataInput = {
event: IChartEvent;
} & Omit<IChartInputWithDates, "events" | 'name'>
} & Omit<IChartInputWithDates, 'events' | 'name'>;
async function getChartData({
chartType,
@@ -365,23 +372,24 @@ async function getChartData({
interval,
startDate,
endDate,
})
});
let result = await db.$queryRawUnsafe<ResultItem[]>(sql);
if(result.length === 0 && breakdowns.length > 0) {
result = await db.$queryRawUnsafe<ResultItem[]>(getChartSql({
chartType,
event,
breakdowns: [],
interval,
startDate,
endDate,
}));
if (result.length === 0 && breakdowns.length > 0) {
result = await db.$queryRawUnsafe<ResultItem[]>(
getChartSql({
chartType,
event,
breakdowns: [],
interval,
startDate,
endDate,
})
);
}
console.log(sql);
// group by sql label
const series = result.reduce(
@@ -401,7 +409,7 @@ async function getChartData({
...acc,
};
},
{} as Record<string, ResultItem[]>,
{} as Record<string, ResultItem[]>
);
return Object.keys(series).map((key) => {
@@ -416,7 +424,7 @@ async function getChartData({
},
totalCount: getTotalCount(data),
data:
chartType === "linear"
chartType === 'linear'
? fillEmptySpotsInTimeline(data, interval, startDate, endDate).map(
(item) => {
return {
@@ -424,7 +432,7 @@ async function getChartData({
count: item.count,
date: new Date(item.date).toISOString(),
};
},
}
)
: [],
};
@@ -435,17 +443,17 @@ function fillEmptySpotsInTimeline(
items: ResultItem[],
interval: string,
startDate: string,
endDate: string,
endDate: string
) {
const result = [];
const clonedStartDate = new Date(startDate);
const clonedEndDate = new Date(endDate);
const today = new Date();
if(interval === 'minute') { 
clonedStartDate.setSeconds(0, 0)
if (interval === 'minute') {
clonedStartDate.setSeconds(0, 0);
clonedEndDate.setMinutes(clonedEndDate.getMinutes() + 1, 0, 0);
} else if (interval === "hour") {
} else if (interval === 'hour') {
clonedStartDate.setMinutes(0, 0, 0);
clonedEndDate.setMinutes(0, 0, 0);
} else {
@@ -455,7 +463,7 @@ function fillEmptySpotsInTimeline(
// Force if interval is month and the start date is the same month as today
const shouldForce = () =>
interval === "month" &&
interval === 'month' &&
clonedStartDate.getFullYear() === today.getFullYear() &&
clonedStartDate.getMonth() === today.getMonth();
@@ -472,20 +480,20 @@ function fillEmptySpotsInTimeline(
const item = items.find((item) => {
const date = new Date(item.date);
if (interval === "month") {
if (interval === 'month') {
return (
getYear(date) === getYear(clonedStartDate) &&
getMonth(date) === getMonth(clonedStartDate)
);
}
if (interval === "day") {
if (interval === 'day') {
return (
getYear(date) === getYear(clonedStartDate) &&
getMonth(date) === getMonth(clonedStartDate) &&
getDay(date) === getDay(clonedStartDate)
);
}
if (interval === "hour") {
if (interval === 'hour') {
return (
getYear(date) === getYear(clonedStartDate) &&
getMonth(date) === getMonth(clonedStartDate) &&
@@ -493,7 +501,7 @@ function fillEmptySpotsInTimeline(
getHour(date) === getHour(clonedStartDate)
);
}
if (interval === "minute") {
if (interval === 'minute') {
return (
getYear(date) === getYear(clonedStartDate) &&
getMonth(date) === getMonth(clonedStartDate) &&
@@ -515,19 +523,19 @@ function fillEmptySpotsInTimeline(
}
switch (interval) {
case "day": {
case 'day': {
clonedStartDate.setDate(clonedStartDate.getDate() + 1);
break;
}
case "hour": {
case 'hour': {
clonedStartDate.setHours(clonedStartDate.getHours() + 1);
break;
}
case "minute": {
case 'minute': {
clonedStartDate.setMinutes(clonedStartDate.getMinutes() + 1);
break;
}
case "month": {
case 'month': {
clonedStartDate.setMonth(clonedStartDate.getMonth() + 1);
break;
}

View File

@@ -1,17 +1,16 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { hashPassword } from "@/server/services/hash.service";
import { randomUUID } from "crypto";
import { getOrganizationBySlug } from "@/server/services/organization.service";
import { randomUUID } from 'crypto';
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { hashPassword } from '@/server/services/hash.service';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { z } from 'zod';
export const clientRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
organizationSlug: z.string(),
}),
})
)
.query(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
@@ -28,7 +27,7 @@ export const clientRouter = createTRPCRouter({
.input(
z.object({
id: z.string(),
}),
})
)
.query(({ input }) => {
return db.client.findUniqueOrThrow({
@@ -42,7 +41,7 @@ export const clientRouter = createTRPCRouter({
z.object({
id: z.string(),
name: z.string(),
}),
})
)
.mutation(({ input }) => {
return db.client.update({
@@ -60,7 +59,7 @@ export const clientRouter = createTRPCRouter({
name: z.string(),
projectId: z.string(),
organizationSlug: z.string(),
}),
})
)
.mutation(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
@@ -83,7 +82,7 @@ export const clientRouter = createTRPCRouter({
.input(
z.object({
id: z.string(),
}),
})
)
.mutation(async ({ input }) => {
await db.client.delete({

View File

@@ -1,9 +1,8 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { getProjectBySlug } from "@/server/services/project.service";
import { slug } from "@/utils/slug";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getProjectBySlug } from '@/server/services/project.service';
import { slug } from '@/utils/slug';
import { z } from 'zod';
export const dashboardRouter = createTRPCRouter({
list: protectedProcedure
@@ -15,12 +14,12 @@ export const dashboardRouter = createTRPCRouter({
.or(
z.object({
projectId: z.string(),
}),
),
})
)
)
.query(async ({ input }) => {
let projectId = null;
if ("projectId" in input) {
if ('projectId' in input) {
projectId = input.projectId;
} else {
projectId = (await getProjectBySlug(input.projectSlug)).id;
@@ -37,7 +36,7 @@ export const dashboardRouter = createTRPCRouter({
z.object({
name: z.string(),
projectId: z.string(),
}),
})
)
.mutation(async ({ input: { projectId, name } }) => {
return db.dashboard.create({

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { z } from 'zod';
export const config = {
api: {
@@ -16,7 +16,7 @@ export const eventRouter = createTRPCRouter({
take: z.number().default(100),
skip: z.number().default(0),
profileId: z.string().optional(),
}),
})
)
.query(async ({ input: { take, skip, projectSlug, profileId } }) => {
const project = await db.project.findUniqueOrThrow({
@@ -29,10 +29,10 @@ export const eventRouter = createTRPCRouter({
skip,
where: {
project_id: project.id,
profile_id: profileId
profile_id: profileId,
},
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
include: {
profile: true,

View File

@@ -1,9 +1,8 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { getOrganizationBySlug } from "@/server/services/organization.service";
import { slug } from "@/utils/slug";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { slug } from '@/utils/slug';
import { z } from 'zod';
export const organizationRouter = createTRPCRouter({
first: protectedProcedure.query(({ ctx }) => {
@@ -21,17 +20,17 @@ export const organizationRouter = createTRPCRouter({
.input(
z.object({
slug: z.string(),
}),
})
)
.query(({ input }) => {
return getOrganizationBySlug(input.slug)
return getOrganizationBySlug(input.slug);
}),
update: protectedProcedure
.input(
z.object({
id: z.string(),
name: z.string(),
}),
})
)
.mutation(({ input }) => {
return db.organization.update({
@@ -40,7 +39,7 @@ export const organizationRouter = createTRPCRouter({
},
data: {
name: input.name,
slug: slug(input.name)
slug: slug(input.name),
},
});
}),

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { z } from 'zod';
export const config = {
api: {
@@ -15,7 +15,7 @@ export const profileRouter = createTRPCRouter({
projectSlug: z.string(),
take: z.number().default(100),
skip: z.number().default(0),
}),
})
)
.query(async ({ input: { take, skip, projectSlug } }) => {
const project = await db.project.findUniqueOrThrow({
@@ -30,7 +30,7 @@ export const profileRouter = createTRPCRouter({
project_id: project.id,
},
orderBy: {
createdAt: "desc",
createdAt: 'desc',
},
});
}),

View File

@@ -1,15 +1,14 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { getOrganizationBySlug } from "@/server/services/organization.service";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getOrganizationBySlug } from '@/server/services/organization.service';
import { z } from 'zod';
export const projectRouter = createTRPCRouter({
list: protectedProcedure
.input(
z.object({
organizationSlug: z.string(),
}),
})
)
.query(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
@@ -23,7 +22,7 @@ export const projectRouter = createTRPCRouter({
.input(
z.object({
id: z.string(),
}),
})
)
.query(({ input }) => {
return db.project.findUniqueOrThrow({
@@ -37,7 +36,7 @@ export const projectRouter = createTRPCRouter({
z.object({
id: z.string(),
name: z.string(),
}),
})
)
.mutation(({ input }) => {
return db.project.update({
@@ -54,7 +53,7 @@ export const projectRouter = createTRPCRouter({
z.object({
name: z.string(),
organizationSlug: z.string(),
}),
})
)
.mutation(async ({ input }) => {
const organization = await getOrganizationBySlug(input.organizationSlug);
@@ -69,7 +68,7 @@ export const projectRouter = createTRPCRouter({
.input(
z.object({
id: z.string(),
}),
})
)
.mutation(async ({ input }) => {
await db.project.delete({

View File

@@ -1,48 +1,54 @@
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { zChartInput } from "@/utils/validation";
import { db } from "@/server/db";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { getDashboardBySlug } from '@/server/services/dashboard.service';
import { getProjectBySlug } from '@/server/services/project.service';
import {
type IChartInput,
type IChartBreakdown,
type IChartEvent,
type IChartEventFilter,
type IChartInput,
type IChartRange,
} from "@/types";
import { type Report as DbReport } from "@prisma/client";
import { getProjectBySlug } from "@/server/services/project.service";
import { getDashboardBySlug } from "@/server/services/dashboard.service";
import { alphabetIds } from "@/utils/constants";
} from '@/types';
import { alphabetIds } from '@/utils/constants';
import { zChartInput } from '@/utils/validation';
import { type Report as DbReport } from '@prisma/client';
import { z } from 'zod';
function transformFilter(filter: Partial<IChartEventFilter>, index: number): IChartEventFilter {
function transformFilter(
filter: Partial<IChartEventFilter>,
index: number
): IChartEventFilter {
return {
id: filter.id ?? alphabetIds[index]!,
name: filter.name ?? 'Unknown Filter',
operator: filter.operator ?? 'is',
value: typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
}
value:
typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
};
}
function transformEvent(event: Partial<IChartEvent>, index: number): IChartEvent {
function transformEvent(
event: Partial<IChartEvent>,
index: number
): IChartEvent {
return {
segment: event.segment ?? 'event',
filters: (event.filters ?? []).map(transformFilter),
id: event.id ?? alphabetIds[index]!,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: event.name || 'Untitled',
}
};
}
function transformReport(report: DbReport): IChartInput & { id: string } {
return {
id: report.id,
events: (report.events as IChartEvent[]).map(transformEvent),
events: (report.events as IChartEvent[]).map(transformEvent),
breakdowns: report.breakdowns as IChartBreakdown[],
chartType: report.chart_type,
interval: report.interval,
name: report.name || 'Untitled',
range: report.range as IChartRange ?? 30,
range: (report.range as IChartRange) ?? 30,
};
}
@@ -51,7 +57,7 @@ export const reportRouter = createTRPCRouter({
.input(
z.object({
id: z.string(),
}),
})
)
.query(({ input: { id } }) => {
return db.report
@@ -67,7 +73,7 @@ export const reportRouter = createTRPCRouter({
z.object({
projectSlug: z.string(),
dashboardSlug: z.string(),
}),
})
)
.query(async ({ input: { projectSlug, dashboardSlug } }) => {
const project = await getProjectBySlug(projectSlug);
@@ -82,7 +88,7 @@ export const reportRouter = createTRPCRouter({
return {
reports: reports.map(transformReport),
dashboard,
}
};
}),
save: protectedProcedure
.input(
@@ -90,7 +96,7 @@ export const reportRouter = createTRPCRouter({
report: zChartInput,
projectId: z.string(),
dashboardId: z.string(),
}),
})
)
.mutation(({ input: { report, projectId, dashboardId } }) => {
return db.report.create({
@@ -105,7 +111,6 @@ export const reportRouter = createTRPCRouter({
range: report.range,
},
});
}),
update: protectedProcedure
.input(
@@ -114,7 +119,7 @@ export const reportRouter = createTRPCRouter({
report: zChartInput,
projectId: z.string(),
dashboardId: z.string(),
}),
})
)
.mutation(({ input: { report, projectId, dashboardId, reportId } }) => {
return db.report.update({

View File

@@ -1,63 +1,59 @@
import { z } from "zod";
import {
createTRPCRouter,
protectedProcedure,
} from "@/server/api/trpc";
import { db } from "@/server/db";
import { hashPassword, verifyPassword } from "@/server/services/hash.service";
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
import { db } from '@/server/db';
import { hashPassword, verifyPassword } from '@/server/services/hash.service';
import { z } from 'zod';
export const userRouter = createTRPCRouter({
current: protectedProcedure.query(({ ctx }) => {
return db.user.findUniqueOrThrow({
where: {
id: ctx.session.user.id
}
})
id: ctx.session.user.id,
},
});
}),
update: protectedProcedure
.input(
z.object({
name: z.string(),
email: z.string(),
}),
})
)
.mutation(({ input, ctx }) => {
return db.user.update({
where: {
id: ctx.session.user.id
id: ctx.session.user.id,
},
data: {
name: input.name,
email: input.email,
}
})
},
});
}),
changePassword: protectedProcedure
.input(
z.object({
password: z.string(),
oldPassword: z.string(),
}),
})
)
.mutation(async ({ input, ctx }) => {
const user = await db.user.findUniqueOrThrow({
where: {
id: ctx.session.user.id
}
})
id: ctx.session.user.id,
},
});
if(!(await verifyPassword(input.oldPassword, user.password))) {
throw new Error('Old password is incorrect')
if (!(await verifyPassword(input.oldPassword, user.password))) {
throw new Error('Old password is incorrect');
}
return db.user.update({
where: {
id: ctx.session.user.id
id: ctx.session.user.id,
},
data: {
password: await hashPassword(input.password),
}
})
},
});
}),
});

View File

@@ -7,14 +7,13 @@
* need to use are documented accordingly near the end.
*/
import { initTRPC, TRPCError } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import { type Session } from "next-auth";
import superjson from "superjson";
import { ZodError } from "zod";
import { getServerAuthSession } from "@/server/auth";
import { db } from "@/server/db";
import { getServerAuthSession } from '@/server/auth';
import { db } from '@/server/db';
import { initTRPC, TRPCError } from '@trpc/server';
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import { type Session } from 'next-auth';
import superjson from 'superjson';
import { ZodError } from 'zod';
/**
* 1. CONTEXT
@@ -109,7 +108,7 @@ export const publicProcedure = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({
ctx: {

View File

@@ -1,14 +1,14 @@
import { type NextApiRequest, type GetServerSidePropsContext } from "next";
import { db } from '@/server/db';
import { verifyPassword } from '@/server/services/hash.service';
import { type GetServerSidePropsContext, type NextApiRequest } from 'next';
import {
getServerSession,
type DefaultSession,
type NextAuthOptions,
} from "next-auth";
} from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import { db } from "@/server/db";
import Credentials from "next-auth/providers/credentials";
import { createError } from "./exceptions";
import { hashPassword, verifyPassword } from "@/server/services/hash.service";
import { createError } from './exceptions';
/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
@@ -16,9 +16,9 @@ import { hashPassword, verifyPassword } from "@/server/services/hash.service";
*
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
*/
declare module "next-auth" {
declare module 'next-auth' {
interface Session extends DefaultSession {
user: DefaultSession["user"] & {
user: DefaultSession['user'] & {
id: string;
};
}
@@ -45,35 +45,35 @@ export const authOptions: NextAuthOptions = {
}),
},
session: {
strategy: "jwt",
strategy: 'jwt',
},
providers: [
Credentials({
name: "Credentials",
name: 'Credentials',
credentials: {
email: { label: "Email", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
email: { label: 'Email', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
if(!credentials?.password || !credentials?.email) {
return null
if (!credentials?.password || !credentials?.email) {
return null;
}
const user = await db.user.findFirst({
where: { email: credentials?.email },
});
if(!user) {
return null
if (!user) {
return null;
}
if(!await verifyPassword(credentials.password, user.password)) {
return null
if (!(await verifyPassword(credentials.password, user.password))) {
return null;
}
return {
...user,
image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby'
image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby',
};
},
}),
@@ -95,22 +95,22 @@ export const authOptions: NextAuthOptions = {
* @see https://next-auth.js.org/configuration/nextjs
*/
export const getServerAuthSession = (ctx: {
req: GetServerSidePropsContext["req"];
res: GetServerSidePropsContext["res"];
req: GetServerSidePropsContext['req'];
res: GetServerSidePropsContext['res'];
}) => {
return getServerSession(ctx.req, ctx.res, authOptions);
};
export async function validateSdkRequest(req: NextApiRequest): Promise<string> {
const clientId = req?.headers["mixan-client-id"] as string | undefined
const clientSecret = req.headers["mixan-client-secret"] as string | undefined
const clientId = req?.headers['mixan-client-id'] as string | undefined;
const clientSecret = req.headers['mixan-client-secret'] as string | undefined;
if (!clientId) {
throw createError(401, "Misisng client id");
throw createError(401, 'Misisng client id');
}
if (!clientSecret) {
throw createError(401, "Misisng client secret");
throw createError(401, 'Misisng client secret');
}
const client = await db.client.findUnique({
@@ -120,14 +120,12 @@ export async function validateSdkRequest(req: NextApiRequest): Promise<string> {
});
if (!client) {
throw createError(401, "Invalid client id");
throw createError(401, 'Invalid client id');
}
if (!(await verifyPassword(clientSecret, client.secret))) {
throw createError(401, "Invalid client secret");
throw createError(401, 'Invalid client secret');
}
return client.project_id
return client.project_id;
}

View File

@@ -1,6 +1,5 @@
import { PrismaClient } from "@prisma/client";
import { env } from "@/env.mjs";
import { env } from '@/env.mjs';
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
@@ -9,8 +8,7 @@ const globalForPrisma = globalThis as unknown as {
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log:
['error']
log: ['error'],
});
if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
if (env.NODE_ENV !== 'production') globalForPrisma.prisma = db;

View File

@@ -1,55 +1,52 @@
import {
type MixanIssue,
type MixanErrorResponse
} from '@mixan/types'
import { type NextApiResponse } from 'next'
import { type NextApiResponse } from 'next';
import { type MixanErrorResponse, type MixanIssue } from '@mixan/types';
export class HttpError extends Error {
public status: number
public message: string
public issues: MixanIssue[]
public status: number;
public message: string;
public issues: MixanIssue[];
constructor(status: number, message: string | Error, issues?: MixanIssue[]) {
super(message instanceof Error ? message.message : message)
this.status = status
this.message = message instanceof Error ? message.message : message
this.issues = issues ?? []
super(message instanceof Error ? message.message : message);
this.status = status;
this.message = message instanceof Error ? message.message : message;
this.issues = issues ?? [];
}
toJson(): MixanErrorResponse {
toJson(): MixanErrorResponse {
return {
code: this.status,
status: 'error',
message: this.message,
issues: this.issues.length ? this.issues : undefined,
stack: process.env.NODE_ENV !== 'production' ? this.stack : undefined,
}
};
}
}
export function createIssues(arr: Array<MixanIssue>) {
throw new HttpError(400, 'Issues', arr)
throw new HttpError(400, 'Issues', arr);
}
export function createError(status = 500, error: unknown) {
if(error instanceof Error || typeof error === 'string') {
return new HttpError(status, error)
}
if (error instanceof Error || typeof error === 'string') {
return new HttpError(status, error);
}
return new HttpError(500, 'Unexpected error occured')
return new HttpError(500, 'Unexpected error occured');
}
export function handleError(res: NextApiResponse, error: unknown) {
if(error instanceof HttpError) {
return res.status(error.status).json(error.toJson())
}
if(error instanceof Error) {
const httpError = createError(500, error)
res.status(httpError.status).json(httpError.toJson())
if (error instanceof HttpError) {
return res.status(error.status).json(error.toJson());
}
const httpError = createError(500, error)
res.status(httpError.status).json(httpError.toJson())
}
if (error instanceof Error) {
const httpError = createError(500, error);
res.status(httpError.status).json(httpError.toJson());
}
const httpError = createError(500, error);
res.status(httpError.status).json(httpError.toJson());
}

View File

@@ -1,45 +1,46 @@
import {
type GetServerSidePropsContext,
type GetServerSidePropsResult,
} from "next";
import { getServerAuthSession } from "./auth";
import { db } from "./db";
} from 'next';
import { getServerAuthSession } from './auth';
import { db } from './db';
export function createServerSideProps(
cb?: (context: GetServerSidePropsContext) => Promise<any>,
cb?: (context: GetServerSidePropsContext) => Promise<any>
) {
return async function getServerSideProps(
context: GetServerSidePropsContext,
context: GetServerSidePropsContext
): Promise<GetServerSidePropsResult<any>> {
const session = await getServerAuthSession(context);
const session = await getServerAuthSession(context);
if(!session) {
if (!session) {
return {
redirect: {
destination: "/api/auth/signin",
destination: '/api/auth/signin',
permanent: false,
},
}
};
}
if(context.params?.organization) {
if (context.params?.organization) {
const organization = await db.user.findFirst({
where: {
id: session.user.id,
organization: {
slug: context.params.organization as string
}
}
})
if(!organization) {
slug: context.params.organization as string,
},
},
});
if (!organization) {
return {
notFound: true,
}
};
}
}
const res = await (typeof cb === "function"
const res = await (typeof cb === 'function'
? cb(context)
: Promise.resolve({}));
return {

View File

@@ -1,9 +1,9 @@
import { db } from "../db";
import { db } from '../db';
export function getDashboardBySlug(slug: string) {
return db.dashboard.findUniqueOrThrow({
where: {
slug
slug,
},
});
}
});
}

View File

@@ -1,4 +1,4 @@
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
import { randomBytes, scrypt, timingSafeEqual } from 'crypto';
const keyLength = 32;
/**
@@ -6,17 +6,17 @@ const keyLength = 32;
* @param {string} password
* @returns {string} The salt+hash
*/
export async function hashPassword (password: string): Promise<string> {
export async function hashPassword(password: string): Promise<string> {
return new Promise((resolve, reject) => {
// generate random 16 bytes long salt - recommended by NodeJS Docs
const salt = randomBytes(16).toString("hex");
const salt = randomBytes(16).toString('hex');
scrypt(password, salt, keyLength, (err, derivedKey) => {
if (err) reject(err);
// derivedKey is of type Buffer
resolve(`${salt}.${derivedKey.toString("hex")}`);
resolve(`${salt}.${derivedKey.toString('hex')}`);
});
});
};
}
/**
* Compare a plain text password with a salt+hash password
@@ -24,11 +24,14 @@ export async function hashPassword (password: string): Promise<string> {
* @param {string} hash The hash+salt to check against
* @returns {boolean}
*/
export async function verifyPassword (password: string, hash: string): Promise<boolean> {
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return new Promise((resolve, reject) => {
const [salt, hashKey] = hash.split(".");
const [salt, hashKey] = hash.split('.');
// we need to pass buffer values to timingSafeEqual
const hashKeyBuff = Buffer.from(hashKey!, "hex");
const hashKeyBuff = Buffer.from(hashKey!, 'hex');
scrypt(password, salt!, keyLength, (err, derivedKey) => {
if (err) {
reject(err);
@@ -37,4 +40,4 @@ export async function verifyPassword (password: string, hash: string): Promise<b
resolve(timingSafeEqual(hashKeyBuff, derivedKey));
});
});
};
}

View File

@@ -1,9 +1,9 @@
import { db } from "../db";
import { db } from '../db';
export function getOrganizationBySlug(slug: string) {
return db.organization.findUniqueOrThrow({
where: {
slug
slug,
},
});
}
});
}

View File

@@ -1,12 +1,12 @@
import { db } from "@/server/db"
import { HttpError } from "@/server/exceptions"
import { db } from '@/server/db';
import { HttpError } from '@/server/exceptions';
export function getProfile(id: string) {
return db.profile.findUniqueOrThrow({
where: {
id,
},
})
});
}
export async function tickProfileProperty({
@@ -14,28 +14,31 @@ export async function tickProfileProperty({
tick,
name,
}: {
profileId: string
tick: number
name: string
profileId: string;
tick: number;
name: string;
}) {
const profile = await getProfile(profileId)
const profile = await getProfile(profileId);
if (!profile) {
throw new HttpError(404, `Profile not found ${profileId}`)
throw new HttpError(404, `Profile not found ${profileId}`);
}
const properties = (
typeof profile.properties === 'object' ? profile.properties ?? {} : {}
) as Record<string, number>
const value = name in properties ? properties[name] : 0
) as Record<string, number>;
const value = name in properties ? properties[name] : 0;
if (typeof value !== 'number') {
throw new HttpError(400, `Property "${name}" on user is of type ${typeof value}`)
throw new HttpError(
400,
`Property "${name}" on user is of type ${typeof value}`
);
}
if (typeof tick !== 'number') {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
throw new HttpError(400, `Value is not a number ${tick} (${typeof tick})`)
throw new HttpError(400, `Value is not a number ${tick} (${typeof tick})`);
}
await db.profile.update({
@@ -48,5 +51,5 @@ export async function tickProfileProperty({
[name]: value + tick,
},
},
})
}
});
}

View File

@@ -1,9 +1,9 @@
import { db } from "../db";
import { db } from '../db';
export function getProjectBySlug(slug: string) {
return db.project.findUniqueOrThrow({
where: {
slug
slug,
},
});
}
});
}