Compare commits

...

61 Commits

Author SHA1 Message Date
cba647ca27 randomize result
All checks were successful
backend-action / build-image (push) Successful in 11m53s
2024-06-24 15:16:47 +07:00
f6b18d3f82 apply limit to resulting array
All checks were successful
backend-action / build-image (push) Successful in 6m21s
2024-06-24 00:23:42 +07:00
d660fd12fa update province user code 2024-06-24 00:23:16 +07:00
38f769ef67 remove admin workflow
All checks were successful
backend-action / build-image (push) Successful in 10m41s
2024-06-08 10:52:58 +07:00
b152175499 change filter method
Some checks failed
backend-admin-action / build-image (push) Failing after 41s
backend-action / build-image (push) Has been cancelled
2024-06-08 10:51:53 +07:00
e0655a56d7 update top three to match filter condition
Some checks failed
backend-admin-action / build-image (push) Failing after 45s
backend-action / build-image (push) Successful in 13m59s
2024-06-08 10:32:37 +07:00
3d6de91dc8 added locking mechanism
Some checks failed
backend-admin-action / build-image (push) Failing after 38s
backend-action / build-image (push) Successful in 7m35s
2024-06-08 10:04:49 +07:00
b28aae201a update db schema + populate allUser 2024-06-08 09:44:53 +07:00
51f52c0a1e make search api public
Some checks failed
backend-admin-action / build-image (push) Failing after 2m33s
backend-action / build-image (push) Successful in 17m31s
2024-06-06 12:15:09 +07:00
decd9a6945 increase timeout
Some checks failed
backend-admin-action / build-image (push) Failing after 38s
backend-action / build-image (push) Successful in 1h53m1s
2024-05-27 11:30:13 +07:00
bb69b33481 increase timeout for cicd
Some checks failed
backend-admin-action / build-image (push) Failing after 42s
backend-action / build-image (push) Has been cancelled
2024-05-27 10:57:08 +07:00
a7eb7d5037 show phone number in api
Some checks failed
backend-admin-action / build-image (push) Failing after 52s
backend-action / build-image (push) Failing after 11m23s
2024-05-27 10:42:43 +07:00
96129c1fe9 added totalUserDeep
Some checks failed
backend-admin-action / build-image (push) Failing after 2m8s
backend-action / build-image (push) Successful in 3m22s
2024-05-25 08:43:12 +07:00
b990b04902 remove public folder clone
Some checks failed
backend-admin-action / build-image (push) Failing after 11m5s
backend-action / build-image (push) Failing after 11m38s
2024-05-20 17:06:32 +07:00
577d97cfcd remove target from admin workflow
Some checks failed
backend-action / build-image (push) Failing after 8s
backend-admin-action / build-image (push) Failing after 7s
2024-05-20 16:51:13 +07:00
23f37df217 added sorvor admin page
Some checks failed
backend-admin-action / build-image (push) Failing after 8s
backend-action / build-image (push) Failing after 1m40s
2024-05-20 16:48:06 +07:00
98a65043c9 add total to zone
Some checks failed
backend-action / build-image (push) Failing after 10m40s
2024-05-20 15:26:18 +07:00
4934f799f5 disable user verification change
All checks were successful
backend-action / build-image (push) Successful in 6m24s
2024-05-20 14:49:44 +07:00
2a69ff0f3e fix remove bug
All checks were successful
backend-action / build-image (push) Successful in 28s
2024-05-20 14:11:50 +07:00
64ea2e9524 add removation
All checks were successful
backend-action / build-image (push) Successful in 8m1s
2024-05-20 13:50:14 +07:00
a29bac1eb2 redo summary
All checks were successful
backend-action / build-image (push) Successful in 7m29s
2024-05-20 12:57:40 +07:00
9a54c4712e return summary
Some checks failed
backend-action / build-image (push) Has been cancelled
2024-05-20 12:56:36 +07:00
ca130dc2b8 update admin route
Some checks failed
backend-action / build-image (push) Has been cancelled
2024-05-20 12:34:54 +07:00
5c739bcfa4 fix initial data + workflow
All checks were successful
backend-action / build-image (push) Successful in 3m40s
2024-05-18 10:04:39 +07:00
7e8d8dc523 fix key typo
Some checks failed
backend-action / build-image (push) Has been cancelled
2024-05-18 09:57:47 +07:00
60a7753247 rename to key
Some checks failed
backend-action / build-image (push) Failing after 1m6s
2024-05-18 09:53:57 +07:00
d54649893c added update to server script
Some checks failed
backend-action / build-image (push) Failing after 1m8s
2024-05-18 09:51:27 +07:00
0f6ec77dc0 update to wal mode
All checks were successful
backend-action / build-image (push) Successful in 58s
2024-05-18 09:40:43 +07:00
99f9531d32 added registrationno field
All checks were successful
backend-action / build-image (push) Successful in 1m6s
2024-05-17 15:55:34 +07:00
3c37fbf59b temporary server action for group and inside 2024-05-17 15:33:15 +07:00
c1a019a461 lint
All checks were successful
backend-action / build-image (push) Successful in 1m9s
2024-05-16 17:26:27 +07:00
5c4abf24bb added linting
added linting
2024-05-16 17:12:19 +07:00
d125687536 added grouping creator
All checks were successful
backend-action / build-image (push) Successful in 1m5s
2024-05-16 14:49:42 +07:00
05f3e019a8 added location element
All checks were successful
backend-action / build-image (push) Successful in 1m50s
2024-05-16 12:50:14 +07:00
6d6bef8f50 added nextjs 2024-05-16 10:40:04 +07:00
6129f489dd add random to display
All checks were successful
backend-action / build-image (push) Successful in 26s
2024-05-13 13:56:44 +07:00
1d834f3a51 override verified status
All checks were successful
backend-action / build-image (push) Successful in 25s
2024-05-10 12:53:46 +07:00
f7bcb2f476 added verified field
All checks were successful
backend-action / build-image (push) Successful in 26s
2024-05-10 12:32:23 +07:00
0cf9d60991 caddy now allow only certain ip 2024-05-10 12:28:22 +07:00
8f90a5031a make most user route a protected procedure
All checks were successful
backend-action / build-image (push) Successful in 29s
2024-05-10 09:57:53 +07:00
e9b53e3e82 Revert "removed cors"
All checks were successful
backend-action / build-image (push) Successful in 24s
This reverts commit 8854022fac.
2024-05-10 09:29:23 +07:00
826029ace8 update lock file
All checks were successful
backend-action / build-image (push) Successful in 34s
2024-05-10 09:06:20 +07:00
8854022fac removed cors
Some checks failed
backend-action / build-image (push) Failing after 9s
2024-05-10 09:04:31 +07:00
be42901480 update choice for opinion
All checks were successful
backend-action / build-image (push) Successful in 42s
2024-05-02 17:19:32 +07:00
0889c2426d updated caddy file
All checks were successful
backend-action / build-image (push) Successful in 29s
2024-04-22 13:08:34 +07:00
eee12e5872 added bouncer
All checks were successful
backend-action / build-image (push) Successful in 42s
2024-04-22 12:56:02 +07:00
63a5ad8a00 added crowdsec
All checks were successful
backend-action / build-image (push) Successful in 36s
2024-04-22 12:02:44 +07:00
948870852e added logs to caddy
All checks were successful
backend-action / build-image (push) Successful in 23s
2024-04-22 10:45:43 +07:00
f6e68d4117 fix: more strict equality check
All checks were successful
backend-action / build-image (push) Successful in 27s
2024-04-21 22:09:40 +07:00
95068907f9 added getAllUserCount
All checks were successful
backend-action / build-image (push) Successful in 28s
2024-04-21 22:03:17 +07:00
c00fbbaea2 added province filter 2024-04-21 21:43:56 +07:00
9ac18c30b8 fix image to show correct path
All checks were successful
backend-action / build-image (push) Successful in 27s
2024-04-21 02:09:47 +07:00
35880519da added getting user and self
All checks were successful
backend-action / build-image (push) Successful in 27s
2024-04-21 02:05:41 +07:00
f159e3da60 added image file
All checks were successful
backend-action / build-image (push) Successful in 41s
2024-04-21 01:54:13 +07:00
490cbf1156 added opinions 2024-04-21 01:53:10 +07:00
da9aa6b87b added workflows 2024-04-20 23:12:26 +07:00
65987354fe removed version 2024-04-20 22:37:38 +07:00
472ae5252f change image location 2024-04-20 17:43:03 +07:00
53688ef8cd change image to match location 2024-04-20 17:36:06 +07:00
6bd525f6b8 added caddy 2024-04-20 16:54:12 +07:00
d12777a747 make bucket accesible 2024-04-20 15:45:10 +07:00
55 changed files with 4399 additions and 283 deletions

View File

@@ -1,6 +1,14 @@
Dockerfile
.dockerignore
.git
minio minio
node_modules node_modules
sqlite.db sqlite.db
sqlite.db-wal
sqlite.db-shm
.DS_Store .DS_Store
dist dist
.env .env
caddy/data/caddy
caddy/config/caddy
testaction.secret

10
.eslintrc.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": [
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"prefer-const": "error"
}
}

View File

@@ -0,0 +1,34 @@
name: backend-action
run-name: ${{ gitea.actor }} is building docker image 🚀
on: [push]
jobs:
build-image:
runs-on: ubuntu-20.04
steps:
- name: Check out repository code
uses: actions/checkout@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
registry: gitea.cognizata.com
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
platforms: linux/amd64
tags: gitea.cognizata.com/atapy/sorvor:latest
target: app
- name: update server
uses: https://github.com/appleboy/ssh-action@v1.0.3
with:
host: 46.102.174.196
username: root
key: ${{ secrets.SORVOR_KEY }}
port: 22
script: "docker compose pull && docker compose up -d"
command_timeout: 30h
- run: echo "🍏 This job's status is ${{ job.status }}."

10
.gitignore vendored
View File

@@ -4,3 +4,13 @@ sqlite.db
.DS_Store .DS_Store
dist dist
.env .env
caddy/data/caddy
caddy/config/caddy
testaction.secret
caddy/logs
.next
sqlite.db-shm
sqlite.db-wal
user.json
user-p.json
user-c.json

View File

@@ -11,9 +11,16 @@ FROM base AS build
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm run build RUN pnpm run build
FROM base AS app FROM node:20-slim AS app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV NODE_ENV="production"
RUN corepack enable
WORKDIR /app
COPY --from=prod-deps /app/node_modules /app/node_modules COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist COPY --from=build /app/dist /app/dist
COPY --from=base /app/package.json /app/package.json
COPY --from=base /app/drizzle /app/drizzle
EXPOSE 3000 EXPOSE 3000
EXPOSE 3001 EXPOSE 3001
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

View File

@@ -1,3 +1,5 @@
1. `pnpm install` 1. `pnpm install`
2. `pnpm initialize_data` 2. `pnpm initialize_data`
3. `docker compose up` 3. `docker compose up`
4. `docker compose exec crowdsec cscli bouncers add CaddyBouncer` to get api key
5. add api key to `CROWDSEC_API_KEY=<KEY>` in `.env` file

View File

@@ -24,3 +24,6 @@ tasks:
- node -r @swc-node/register src/app.ts - node -r @swc-node/register src/app.ts
env: env:
NODE_ENV: production NODE_ENV: production
update-server:
cmds:
- ssh -t sorvor-p "docker compose pull && docker compose up -d"

View File

@@ -1,12 +1,36 @@
import { db } from "./src/db"; import { db } from "./src/db";
import { group, opinion, zone, province } from "./src/schema"; import {
group,
opinion,
zone,
province,
user,
userToSelection,
} from "./src/schema";
import { Groups, Opinions, Provinces, Districts } from "./initialData"; import { Groups, Opinions, Provinces, Districts } from "./initialData";
import { createBucket, createClient } from "./src/minio"; import { createBucket, createClient } from "./src/minio";
import { Config } from "./src/config";
import ud from "./user-c.json";
const user_data: UserData[] = ud;
console.log(ud.length);
for (const user of user_data) {
let thisName = `${user.first_name} ${user.last_name}`;
if (
user_data.filter((u) => `${u.first_name} ${u.last_name}` == thisName)
.length != 1
) {
console.log(`duplicate name ${user}`);
}
}
async function main() { async function main() {
let mc = createClient(); try {
await createBucket(mc); await setupBucket();
} catch {
console.error("Setting up bucket failed");
}
const isInitialized = await db.query.group const isInitialized = await db.query.group
.findMany() .findMany()
.then((groups) => groups.length > 0); .then((groups) => groups.length > 0);
@@ -32,7 +56,116 @@ async function main() {
province: district.province_code, province: district.province_code,
})); }));
await db.insert(zone).values(zoneValues); await db.insert(zone).values(zoneValues);
await create_user();
await create_relation();
const allUser = await db.query.user.findMany({
with: {
userToSelection: { with: { selection: true } },
},
});
// for (const u of allUser) {
// console.log(
// u.firstName,
// u.lastName,
// u.userToSelection.map(
// (t) => `${t.selection?.firstName} ${t.selection?.lastName}`,
// ),
// );
// }
console.log("Done"); console.log("Done");
} }
type UserData = {
first_name: string;
last_name: string;
job_code: number;
selection: string[];
province: string;
rank: number;
};
async function create_user() {
const provinces = await db.query.province.findMany({});
const district = await db.query.zone.findMany({});
for (const newUser of user_data) {
let isSelectionFound = true;
for (const selection of newUser.selection) {
const isFound = user_data.findIndex(
(p) => `${p.first_name} ${p.last_name}` == selection,
);
if (isFound == -1) {
isSelectionFound = false;
}
}
if (!isSelectionFound) {
console.log(newUser.province, newUser, isSelectionFound);
} else {
await db.insert(user).values({
group: newUser.job_code,
firstName: newUser.first_name,
lastName: newUser.last_name,
title: "",
cid: "0000000000000",
phone: "0000000000",
age: 0,
job: "",
education: "",
zone: 1001,
rank: newUser.rank,
});
}
}
}
async function create_relation() {
const allUser = await db.query.user.findMany({
with: {
zone: { with: { province: true } },
},
});
for (const u of allUser) {
let thisUsers = user_data.filter(
(raw) => raw.first_name == u.firstName && raw.last_name == u.lastName,
);
if (thisUsers.length !== 1) {
console.log("duplicated users", thisUsers);
return;
}
const rawUser = thisUsers[0];
const selections = allUser.filter(
(target) =>
rawUser.selection.includes(`${target.firstName} ${target.lastName}`) &&
target.zone.province.id == u.zone.province.id,
);
if (selections.length == 0) {
console.log("selection not found", selections);
return;
}
for (const selection of selections) {
await db
.insert(userToSelection)
.values({ userId: u.id, targetId: selection.id });
}
}
}
async function setupBucket() {
const BucketPolicy = {
Version: "2012-10-17",
Statement: [
{
Action: ["s3:GetObject"],
Effect: "Allow",
Principal: "*",
Resource: [`arn:aws:s3:::${Config.bucketName}/*`],
Sid: "AllowAnonymousAccess",
},
],
};
let mc = createClient();
await createBucket(mc);
await mc.setBucketPolicy(Config.bucketName, JSON.stringify(BucketPolicy));
}
main(); main();

3
app/global.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,79 @@
"use client";
import { LocationContext } from "@/components/locationContext";
import { useContext, useState } from "react";
import Grouping from "./Grouping";
import { Group, JobCategory, updateGroups } from "./action";
type Props = {
allJobs: JobCategory[];
};
export default function GroupCreator({ allJobs }: Props) {
const locationContext = useContext(LocationContext);
const [usedJobs, setUsedJobs] = useState<number[]>([]);
const [groups, setGroup] = useState<Group[]>(
[...Array(4).keys()].map((i) => ({ id: i + 1, jobs: [] })),
);
function useJob(id: number) {
setUsedJobs((u) => [...u, id]);
}
function freeJob(id: number) {
setUsedJobs((u) => u.filter((j) => j != id));
}
function updateGroup(id: number, jobs: number[]) {
setGroup((groups) =>
groups.map((g) => {
if (g.id != id) return g;
else return { id, jobs };
}),
);
}
async function submit() {
if (
locationContext?.zone[0] == undefined ||
locationContext?.province[0] == undefined
) {
alert("ยังไม่ได้เลือกพื้นที่");
return;
}
await updateGroups(
locationContext.province[0],
locationContext.zone[0],
groups,
);
alert("อัพเดทสำเร็จ");
}
if (
locationContext?.zone[0] == undefined ||
locationContext?.province[0] == undefined
) {
return undefined;
}
return (
<div>
{groups.map((g) => (
<div className="flex flex-col items-center" key={g.id}>
<p>: {g.id}</p>
<Grouping
availableJobs={allJobs.filter((j) => !usedJobs.includes(j.id))}
selectJob={useJob}
removeJob={freeJob}
updateGroup={(ids) => updateGroup(g.id, ids)}
/>
</div>
))}
<div className="flex justify-center">
<button className="rounded-md bg-green-300 p-2" onClick={submit}>
</button>
</div>
</div>
);
}

60
app/grouping/Grouping.tsx Normal file
View File

@@ -0,0 +1,60 @@
import { useState } from "react";
import { JobCategory } from "./action";
type Props = {
availableJobs: JobCategory[];
selectJob: (id: number) => void;
removeJob: (id: number) => void;
updateGroup: (id: number[]) => void;
};
export default function Grouping({
availableJobs,
selectJob,
removeJob,
updateGroup,
}: Props) {
const [selectedJob, setSelectedJob] = useState<JobCategory[]>([]);
function addJob(id: string) {
const _id = parseInt(id);
const job = availableJobs.find((j) => j.id == _id);
if (job == undefined) return;
const newSelectedJob = [...selectedJob, job];
setSelectedJob(newSelectedJob);
selectJob(_id);
updateGroup(newSelectedJob.map((j) => j.id));
}
function removeJobFromGroup(id: number) {
const newSelectedJob = selectedJob.filter((j) => j.id != id);
setSelectedJob(newSelectedJob);
removeJob(id);
updateGroup(newSelectedJob.map((j) => j.id));
}
return (
<div className="m-2 flex w-full flex-col gap-2 rounded-md border border-black p-2 shadow-md">
{selectedJob.map((j) => (
<div className="flex justify-between gap-2 p-2" key={j.id}>
<p>{j.name}</p>
<button
onClick={() => removeJobFromGroup(j.id)}
className="text-red-400"
>
</button>
</div>
))}
{selectedJob.length < 5 && (
<select onChange={(e) => addJob(e.currentTarget.value)}>
<option value={undefined} hidden>
None
</option>
{availableJobs.map((j) => (
<option value={j.id} key={j.id}>
{j.name}
</option>
))}
</select>
)}
</div>
);
}

20
app/grouping/action.ts Normal file
View File

@@ -0,0 +1,20 @@
"use server";
export type JobCategory = {
id: number;
name: string;
};
export type Group = {
id: number;
jobs: number[];
};
//TODO: submit group
export async function updateGroups(
province: number,
zone: number,
groups: Group[],
) {
console.log({ province, zone, groups });
}

24
app/grouping/page.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { db } from "@/src/db";
import LocationSelector from "../../components/LocationSelector";
import LocationContextProvider from "@/components/locationContext";
import GroupCreator from "./GroupCreator";
import { eq } from "drizzle-orm";
import { user } from "@/src/schema";
export default async function Page() {
const provinces = await db.query.province
.findMany({ with: { zones: true } })
.execute();
const jobList = await db.query.group.findMany().execute();
const r = await db.query.user
.findMany({ columns: { id: true }, where: eq(user.verified, true) })
.execute()
.then((v) => v.length);
console.log(r);
return (
<LocationContextProvider>
<LocationSelector provinces={provinces} />
<GroupCreator allJobs={jobList} />
</LocationContextProvider>
);
}

View File

@@ -0,0 +1,89 @@
import { useEffect, useState } from "react";
type Props = {
updateIdList: (cids: string[]) => void;
};
export default function IdComponent({ updateIdList }: Props) {
const [idSet, setIdSet] = useState<Set<string>>(new Set());
const onValidId = (id: string) => {
const newSet = new Set(idSet.add(id));
setIdSet(newSet);
updateIdList([...newSet]);
};
const removeCid = (id: string) => {
const newSet = new Set([...idSet].filter((x) => x !== id));
setIdSet(newSet);
updateIdList([...newSet]);
};
return (
<div className="flex justify-center">
<div className="flex flex-col gap-2">
{Array.from(idSet).map((v) => (
<FixedId cid={v} removeCid={removeCid} key={v} />
))}
<SingleIdComponent onValidId={onValidId} />
</div>
</div>
);
}
type FixedIdProps = {
cid: string;
removeCid: (cid: string) => void;
};
function FixedId({ cid, removeCid }: FixedIdProps) {
return (
<div className="flex gap-2">
<input type="text" className="border-2" disabled value={cid} />
<button
className="rounded-md bg-red-300 p-2"
onClick={() => removeCid(cid)}
>
</button>
</div>
);
}
type SingleIdProps = {
onValidId: (cid: string) => void;
};
function SingleIdComponent({ onValidId }: SingleIdProps) {
const [isValid, setIsValid] = useState(false);
const [cid, setCid] = useState("");
useEffect(() => {
const isValidId = isValidThaiID(cid);
setIsValid(isValidId);
if (isValidId) {
onValidId(cid);
setCid("");
}
}, [cid, onValidId]);
return (
<div className="flex gap-2">
<input
type="text"
className="border-2"
onChange={(v) => setCid(v.target.value)}
value={cid}
/>
{isValid ? <p>OK</p> : null}
</div>
);
}
function isValidThaiID(id: string) {
if (!/^\d{13}$/.test(id)) {
return false;
}
let sum = 0;
for (let i = 0; i < 12; i++) {
sum += Number(id[i]) * (13 - i);
}
const checkSum = (11 - (sum % 11)) % 10;
return checkSum === Number(id[12]);
}

13
app/inside/action.ts Normal file
View File

@@ -0,0 +1,13 @@
"use server";
import { db } from "@/src/db";
import { user } from "@/src/schema";
import { inArray } from "drizzle-orm";
export async function saveUser(cids: string[]) {
const rs = await db
.update(user)
.set({ verified: true })
.where(inArray(user.cid, cids))
.execute();
return rs;
}

23
app/inside/page.tsx Normal file
View File

@@ -0,0 +1,23 @@
"use client";
import { useState } from "react";
import { saveUser } from "./action";
import IdComponent from "./IdComponent";
export default function Page() {
const [idList, setIdList] = useState<string[]>([]);
async function submit() {
const rs = await saveUser(idList);
alert(`อัพเดทสำเร็จ ${rs.changes} คน`);
}
return (
<div>
<IdComponent updateIdList={(cids) => setIdList(cids)} />
<p className="mt-2 flex items-center justify-center gap-4">
Total: {idList.length}{" "}
<button className="rounded-md bg-green-200 p-2" onClick={submit}>
Submit
</button>
</p>
</div>
);
}

17
app/layout.tsx Normal file
View File

@@ -0,0 +1,17 @@
import "./global.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div className="container mx-auto mt-2 p-2">{children}</div>
</body>
</html>
);
}

3
app/page.tsx Normal file
View File

@@ -0,0 +1,3 @@
export default function Page() {
return <h1>Hello!</h1>;
}

47
app/total/TotalSetter.tsx Normal file
View File

@@ -0,0 +1,47 @@
"use client";
import { LocationContext } from "@/components/locationContext";
import { useContext, useState } from "react";
import { updateZone } from "./action";
export default function TotalSetter() {
const locationContext = useContext(LocationContext);
const [total, setTotal] = useState<number>(0);
async function submit() {
if (
locationContext?.zone[0] == undefined ||
locationContext?.province[0] == undefined
) {
alert("ยังไม่ได้เลือกพื้นที่");
return;
}
await updateZone(
locationContext.province[0],
locationContext.zone[0],
total,
);
alert(`อัพเดทสำเร็จ`);
}
return (
<div>
<div className="flex">
:{" "}
<input
value={total}
className="rounded-md border-2"
type="number"
onChange={(e) =>
setTotal(
e.currentTarget.value !== ""
? parseInt(e.currentTarget.value)
: 0,
)
}
/>
</div>
<button className="rounded-md bg-green-300 p-2" onClick={submit}>
</button>
</div>
);
}

20
app/total/action.ts Normal file
View File

@@ -0,0 +1,20 @@
"use server";
export type JobCategory = {
id: number;
name: string;
};
export type Group = {
id: number;
jobs: number[];
};
//TODO: submit group
export async function updateZone(
province: number,
zone: number,
total: number,
) {
console.log({ province, zone, total });
}

16
app/total/page.tsx Normal file
View File

@@ -0,0 +1,16 @@
import { db } from "@/src/db";
import LocationSelector from "../../components/LocationSelector";
import LocationContextProvider from "@/components/locationContext";
import TotalSetter from "./TotalSetter";
export default async function Page() {
const provinces = await db.query.province
.findMany({ with: { zones: true } })
.execute();
return (
<LocationContextProvider>
<LocationSelector provinces={provinces} />
<TotalSetter />
</LocationContextProvider>
);
}

27
caddy/Caddyfile Normal file
View File

@@ -0,0 +1,27 @@
{
crowdsec {
api_url {$CROWDSEC_API_URL}
api_key {$CROWDSEC_API_KEY}
}
}
{$CADDY_BASE_HOST} {
@blocked not remote_ip {$CADDY_ALLOWED_IPS}
route {
crowdsec
respond @blocked "<h1>Access Denied</h1>" 403
}
encode zstd gzip
reverse_proxy http://frontend:3000
handle_path /api/* {
reverse_proxy http://backend:3000
}
handle_path /profileImages/* {
rewrite * /sorvor{path}
reverse_proxy http://minio:9000
}
log {
output file /var/log/caddy/access.log
}
}

14
caddy/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
ARG CADDY_VERSION=2
FROM caddy:${CADDY_VERSION}-builder-alpine AS builder
RUN xcaddy build \
--with github.com/mholt/caddy-l4 \
--with github.com/hslatman/caddy-crowdsec-bouncer/http@main \
--with github.com/hslatman/caddy-crowdsec-bouncer/layer4@main
FROM caddy:${CADDY_VERSION} AS caddy
WORKDIR /
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

View File

@@ -0,0 +1,88 @@
"use client";
import { useContext, useEffect, useState } from "react";
import { LocationContext } from "../locationContext";
type Props = {
provinces: Province[];
};
type Province = {
id: number;
name: string;
zones: Zone[];
};
type Zone = {
id: number;
name: string;
province: number;
};
export default function LocationSelector({ provinces }: Props) {
const [provinceId, setProvinceId] = useState<number | undefined>(undefined);
const [amphurList, setAmphurList] = useState<Zone[] | undefined>(undefined);
const [amphurId, setAmphurId] = useState<number | undefined>(undefined);
const locationContext = useContext(LocationContext);
function setProvince(_id: string) {
const id = parseInt(_id);
setProvinceId(id);
const province = provinces.find((p) => p.id == id);
if (province == undefined) return;
setAmphurList(province.zones);
setAmphurId(undefined);
}
function setAmphur(_id: string) {
const id = parseInt(_id);
setAmphurId(id);
}
useEffect(() => {
if (locationContext == undefined) return;
if (amphurId == undefined || provinceId == undefined) {
locationContext.zone[1](undefined);
locationContext.province[1](undefined);
return;
}
locationContext.zone[1](amphurId);
locationContext.province[1](provinceId);
}, [amphurId, locationContext, provinceId]);
return (
<div className="flex flex-col gap-2">
<div className="flex gap-2">
:
<select
value={provinceId}
className="flex-1"
onChange={(e) => setProvince(e.currentTarget.value)}
>
<option value={undefined} hidden>
None
</option>
{provinces.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
{amphurList && (
<div className="flex gap-2">
:
<select
value={amphurId}
className="flex-1"
onChange={(e) => setAmphur(e.currentTarget.value)}
>
<option value={undefined} hidden>
None
</option>
{amphurList.map((a) => (
<option key={a.id} value={a.id}>
{a.name}
</option>
))}
</select>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,32 @@
"use client";
import {
Dispatch,
ReactNode,
SetStateAction,
createContext,
useState,
} from "react";
type LocationContextType = {
zone: [number | undefined, Dispatch<SetStateAction<number | undefined>>];
province: [number | undefined, Dispatch<SetStateAction<number | undefined>>];
};
export const LocationContext = createContext<LocationContextType | undefined>(
undefined,
);
export default function LocationContextProvider({
children,
}: {
children?: ReactNode;
}) {
const zone = useState<number | undefined>(undefined);
const province = useState<number | undefined>(undefined);
return (
<LocationContext.Provider value={{ zone, province }}>
{children}
</LocationContext.Provider>
);
}

View File

@@ -1,7 +1,7 @@
version: "3"
services: services:
backend: backend:
restart: always restart: always
image: gitea.cognizata.com/atapy/sorvor
build: build:
target: app # choose a stage to use target: app # choose a stage to use
ports: ports:
@@ -9,10 +9,23 @@ services:
- 3001:3001 - 3001:3001
volumes: volumes:
- ./sqlite.db:/app/sqlite.db - ./sqlite.db:/app/sqlite.db
- ./sqlite.db-shm:/app/sqlite.db-shm
- ./sqlite.db-wal:/app/sqlite.db-wal
env_file: env_file:
- .env - .env
admin:
build:
dockerfile: ./next.Dockerfile
ports:
- 3003:3000
volumes:
- ./sqlite.db:/app/sqlite.db
- ./sqlite.db-shm:/app/sqlite.db-shm
- ./sqlite.db-wal:/app/sqlite.db-wal
frontend: frontend:
restart: always restart: always
image: gitea.cognizata.com/atapy/sorvor-front
build: build:
context: ../sorvor-front context: ../sorvor-front
args: args:
@@ -30,3 +43,35 @@ services:
- ./minio/data:/data - ./minio/data:/data
env_file: env_file:
- .env - .env
caddy:
build:
context: ./caddy/
target: caddy
restart: unless-stopped
env_file:
- .env
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile
- ./caddy/data/:/data
- ./caddy/config/:/config
- ./caddy/logs/:/var/log/caddy/
crowdsec:
image: crowdsecurity/crowdsec
environment:
COLLECTIONS: "crowdsecurity/caddy"
depends_on:
- "caddy"
volumes:
- ./caddy/logs:/var/log/caddy
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
volumes:
crowdsec-db:
crowdsec-config:

4
crowdsec/acquis.yaml Normal file
View File

@@ -0,0 +1,4 @@
filenames:
- /var/log/caddy/access.log
labels:
type: caddy

View File

@@ -3,16 +3,17 @@ CREATE TABLE `groups` (
`name` text NOT NULL `name` text NOT NULL
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `image_to_user` (
`user_id` integer PRIMARY KEY NOT NULL,
`image_name` text NOT NULL,
`created_on` integer DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE `opinions` ( CREATE TABLE `opinions` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`type` text DEFAULT '3Choice' NOT NULL `type` text DEFAULT '5Choice' NOT NULL
);
--> statement-breakpoint
CREATE TABLE `phone_tokens` (
`phone` text PRIMARY KEY NOT NULL,
`token` text NOT NULL,
`created_on` integer DEFAULT CURRENT_TIMESTAMP
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `provinces` ( CREATE TABLE `provinces` (
@@ -24,6 +25,7 @@ CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`firstName` text NOT NULL, `firstName` text NOT NULL,
`lastName` text NOT NULL, `lastName` text NOT NULL,
`registerno` text,
`title` text NOT NULL, `title` text NOT NULL,
`cid` text(13) NOT NULL, `cid` text(13) NOT NULL,
`age` integer NOT NULL, `age` integer NOT NULL,
@@ -33,6 +35,7 @@ CREATE TABLE `users` (
`twitter` text, `twitter` text,
`tiktok` text, `tiktok` text,
`other_social` text, `other_social` text,
`image` text,
`email` text, `email` text,
`job` text NOT NULL, `job` text NOT NULL,
`education` text NOT NULL, `education` text NOT NULL,
@@ -40,6 +43,7 @@ CREATE TABLE `users` (
`reason` text, `reason` text,
`group_id` integer NOT NULL, `group_id` integer NOT NULL,
`zone_id` integer NOT NULL, `zone_id` integer NOT NULL,
`verified` integer DEFAULT false NOT NULL,
FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`group_id`) REFERENCES `groups`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`zone_id`) REFERENCES `zones`(`id`) ON UPDATE no action ON DELETE no action FOREIGN KEY (`zone_id`) REFERENCES `zones`(`id`) ON UPDATE no action ON DELETE no action
); );
@@ -47,23 +51,30 @@ CREATE TABLE `users` (
CREATE TABLE `user_opinions` ( CREATE TABLE `user_opinions` (
`user_id` integer NOT NULL, `user_id` integer NOT NULL,
`opinion_id` integer NOT NULL, `opinion_id` integer NOT NULL,
`choice` text DEFAULT 'deciding', `choice` text DEFAULT 'ignore',
PRIMARY KEY(`opinion_id`, `user_id`), PRIMARY KEY(`opinion_id`, `user_id`),
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`opinion_id`) REFERENCES `opinions`(`id`) ON UPDATE no action ON DELETE no action FOREIGN KEY (`opinion_id`) REFERENCES `opinions`(`id`) ON UPDATE no action ON DELETE no action
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE `user_to_selection` (
`user_id` integer,
`target_id` integer,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`target_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `zones` ( CREATE TABLE `zones` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL, `name` text NOT NULL,
`province_id` integer NOT NULL, `province_id` integer NOT NULL,
`total` integer DEFAULT 0 NOT NULL,
FOREIGN KEY (`province_id`) REFERENCES `provinces`(`id`) ON UPDATE no action ON DELETE no action FOREIGN KEY (`province_id`) REFERENCES `provinces`(`id`) ON UPDATE no action ON DELETE no action
); );
--> statement-breakpoint --> statement-breakpoint
CREATE UNIQUE INDEX `groups_name_unique` ON `groups` (`name`);--> statement-breakpoint CREATE UNIQUE INDEX `groups_name_unique` ON `groups` (`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `opinions_name_unique` ON `opinions` (`name`);--> statement-breakpoint CREATE UNIQUE INDEX `opinions_name_unique` ON `opinions` (`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `provinces_name_unique` ON `provinces` (`name`);--> statement-breakpoint CREATE UNIQUE INDEX `provinces_name_unique` ON `provinces` (`name`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_cid_unique` ON `users` (`cid`);--> statement-breakpoint
CREATE UNIQUE INDEX `users_phone_unique` ON `users` (`phone`);--> statement-breakpoint
CREATE INDEX `phone_idx` ON `users` (`phone`);--> statement-breakpoint CREATE INDEX `phone_idx` ON `users` (`phone`);--> statement-breakpoint
CREATE INDEX `image_idx` ON `users` (`image`);--> statement-breakpoint
CREATE UNIQUE INDEX `zones_name_province_id_unique` ON `zones` (`name`,`province_id`); CREATE UNIQUE INDEX `zones_name_province_id_unique` ON `zones` (`name`,`province_id`);

View File

@@ -1,10 +0,0 @@
CREATE TABLE `image_to_user` (
`user_id` integer PRIMARY KEY NOT NULL,
`image_name` text NOT NULL,
`created_on` integer DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
DROP TABLE `phone_tokens`;--> statement-breakpoint
ALTER TABLE users ADD `image` text;--> statement-breakpoint
CREATE INDEX `image_idx` ON `users` (`image`);

View File

@@ -0,0 +1 @@
ALTER TABLE users ADD `rank` integer DEFAULT 9999;

View File

@@ -1,7 +1,7 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "58f80520-7300-4bc4-943d-87568666e42d", "id": "b46f7266-bc92-4a23-b12d-d162624db70a",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"tables": { "tables": {
"groups": { "groups": {
@@ -35,6 +35,51 @@
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"image_to_user": {
"name": "image_to_user",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"image_name": {
"name": "image_name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_on": {
"name": "created_on",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {},
"foreignKeys": {
"image_to_user_user_id_users_id_fk": {
"name": "image_to_user_user_id_users_id_fk",
"tableFrom": "image_to_user",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"opinions": { "opinions": {
"name": "opinions", "name": "opinions",
"columns": { "columns": {
@@ -58,7 +103,7 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false, "autoincrement": false,
"default": "'3Choice'" "default": "'5Choice'"
} }
}, },
"indexes": { "indexes": {
@@ -74,37 +119,6 @@
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"phone_tokens": {
"name": "phone_tokens",
"columns": {
"phone": {
"name": "phone",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"created_on": {
"name": "created_on",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"provinces": { "provinces": {
"name": "provinces", "name": "provinces",
"columns": { "columns": {
@@ -160,6 +174,13 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"registerno": {
"name": "registerno",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": { "title": {
"name": "title", "name": "title",
"type": "text", "type": "text",
@@ -223,6 +244,13 @@
"notNull": false, "notNull": false,
"autoincrement": false "autoincrement": false
}, },
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": { "email": {
"name": "email", "name": "email",
"type": "text", "type": "text",
@@ -271,29 +299,30 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"verified": {
"name": "verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
} }
}, },
"indexes": { "indexes": {
"users_cid_unique": {
"name": "users_cid_unique",
"columns": [
"cid"
],
"isUnique": true
},
"users_phone_unique": {
"name": "users_phone_unique",
"columns": [
"phone"
],
"isUnique": true
},
"phone_idx": { "phone_idx": {
"name": "phone_idx", "name": "phone_idx",
"columns": [ "columns": [
"phone" "phone"
], ],
"isUnique": false "isUnique": false
},
"image_idx": {
"name": "image_idx",
"columns": [
"image"
],
"isUnique": false
} }
}, },
"foreignKeys": { "foreignKeys": {
@@ -350,7 +379,7 @@
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": "'deciding'" "default": "'ignore'"
} }
}, },
"indexes": {}, "indexes": {},
@@ -393,6 +422,56 @@
}, },
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"user_to_selection": {
"name": "user_to_selection",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"target_id": {
"name": "target_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_to_selection_user_id_users_id_fk": {
"name": "user_to_selection_user_id_users_id_fk",
"tableFrom": "user_to_selection",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_to_selection_target_id_users_id_fk": {
"name": "user_to_selection_target_id_users_id_fk",
"tableFrom": "user_to_selection",
"tableTo": "users",
"columnsFrom": [
"target_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"zones": { "zones": {
"name": "zones", "name": "zones",
"columns": { "columns": {
@@ -416,6 +495,14 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"total": {
"name": "total",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {

View File

@@ -1,8 +1,8 @@
{ {
"version": "5", "version": "5",
"dialect": "sqlite", "dialect": "sqlite",
"id": "2cf2acb7-cc98-4f28-8ead-5916b87b7683", "id": "d7a1e002-36b8-4c25-b935-c826f0a2aaab",
"prevId": "58f80520-7300-4bc4-943d-87568666e42d", "prevId": "b46f7266-bc92-4a23-b12d-d162624db70a",
"tables": { "tables": {
"groups": { "groups": {
"name": "groups", "name": "groups",
@@ -103,7 +103,7 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false, "autoincrement": false,
"default": "'3Choice'" "default": "'5Choice'"
} }
}, },
"indexes": { "indexes": {
@@ -174,6 +174,13 @@
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
}, },
"registerno": {
"name": "registerno",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"title": { "title": {
"name": "title", "name": "title",
"type": "text", "type": "text",
@@ -292,23 +299,25 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"verified": {
"name": "verified",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"rank": {
"name": "rank",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": 9999
} }
}, },
"indexes": { "indexes": {
"users_cid_unique": {
"name": "users_cid_unique",
"columns": [
"cid"
],
"isUnique": true
},
"users_phone_unique": {
"name": "users_phone_unique",
"columns": [
"phone"
],
"isUnique": true
},
"phone_idx": { "phone_idx": {
"name": "phone_idx", "name": "phone_idx",
"columns": [ "columns": [
@@ -378,7 +387,7 @@
"primaryKey": false, "primaryKey": false,
"notNull": false, "notNull": false,
"autoincrement": false, "autoincrement": false,
"default": "'deciding'" "default": "'ignore'"
} }
}, },
"indexes": {}, "indexes": {},
@@ -421,6 +430,56 @@
}, },
"uniqueConstraints": {} "uniqueConstraints": {}
}, },
"user_to_selection": {
"name": "user_to_selection",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"target_id": {
"name": "target_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"user_to_selection_user_id_users_id_fk": {
"name": "user_to_selection_user_id_users_id_fk",
"tableFrom": "user_to_selection",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"user_to_selection_target_id_users_id_fk": {
"name": "user_to_selection_target_id_users_id_fk",
"tableFrom": "user_to_selection",
"tableTo": "users",
"columnsFrom": [
"target_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"zones": { "zones": {
"name": "zones", "name": "zones",
"columns": { "columns": {
@@ -444,6 +503,14 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"autoincrement": false "autoincrement": false
},
"total": {
"name": "total",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
} }
}, },
"indexes": { "indexes": {

View File

@@ -5,15 +5,15 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1713548458041, "when": 1717814164478,
"tag": "0000_right_nebula", "tag": "0000_tired_impossible_man",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "5", "version": "5",
"when": 1713599233997, "when": 1717817049392,
"tag": "0001_chilly_bullseye", "tag": "0001_last_hiroim",
"breakpoints": true "breakpoints": true
} }
] ]

61
flake.lock generated Normal file
View File

@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1713537308,
"narHash": "sha256-XtTSSIB2DA6tOv+l0FhvfDMiyCmhoRbNB+0SeInZkbk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5c24cf2f0a12ad855f444c30b2421d044120c66f",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

23
flake.nix Normal file
View File

@@ -0,0 +1,23 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
with pkgs;
{
devShells.default = mkShell {
packages = with pkgs; [
act
];
shellHook = ''
'';
};
}
);
}

View File

@@ -1,23 +1,23 @@
export const Groups = [ export const Groups = [
"กลุ่มบริหารราชการแผ่นดินและความมั่นคง", "กลุ่มการบริหารราชการแผ่นดินและความมั่นคง อันได้แก่ ผู้เคยเป็นข้าราชการ เจ้าหน้าที่ของรัฐ หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มกฎหมายและกระบวนการยุติธรรม", "กลุ่มกฎหมายและกระบวนการยุติธรรม อันได้แก่ ผู้เป็นหรือเคยเป็นผู้พิพากษา ตุลาการ อัยการ ตำรวจ ผู้ประกอบวิชาชีพด้านกฎหมาย หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มการศึกษา", "กลุ่มการศึกษา อันได้แก่ ผู้เป็นหรือเคยเป็นครู อาจารย์ นักวิจัย ผู้บริหารสถานศึกษา บุคลากรทางการศึกษา หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มสาธารณสุข", "กลุ่มการสาธารณสุข อันได้แก่ ผู้เป็นหรือเคยเป็นแพทย์ทุกประเภท เทคนิคการแพทย์ สาธารณสุข พยาบาล เภสัชกร หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มทำนา ทำไร่", "กลุ่มอาชีพทำนา ปลูกพืชล้มลุก หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มทำสวน ป่าไม้ ประมง เลี้ยงสัตว์", "กลุ่มอาชีพทำสวน ป่าไม้ ปศุสัตว์ ประมง หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มลูกจ้าง ผู้ใช้แรงงาน", "กลุ่มพนักงานหรือลูกจ้างของบุคคลซึ่งมิใช่ส่วนราชการหรือหน่วยงานของรัฐ ผู้ใช้แรงงาน หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มผู้ประกอบกิจการ SMEs", "กลุ่มผู้ประกอบอาชีพด้านสิ่งแวดล้อม ผังเมือง อสังหาริมทรัพย์และสาธารณูปโภค ทรัพยากรธรรมชาติ พลังงาน หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มผู้ประกอบกิจการอื่น", "กลุ่มผู้ประกอบกิจการขนาดกลางและขนาดย่อมตามกฎหมายว่าด้วยการนั้น หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มผู้ประกอบอุตสาหกรรม", "กลุ่มผู้ประกอบกิจการอื่นนอกจากกิจการตาม (๙)",
"กลุ่มสิ่งแวดล้อม อสังหาริมทรัพย์ พลังงาน", "กลุ่มผู้ประกอบธุรกิจหรืออาชีพด้านการท่องเที่ยว อันได้แก่ ผู้ประกอบธุรกิจท่องเที่ยว มัคคุเทศก์ ผู้ประกอบกิจการหรือพนักงานโรงแรม หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มท่องเที่ยว โรงแรม", "กลุ่มผู้ประกอบอุตสาหกรรม หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มวิทยาศาสตร์ เทคโนโลยี", "กลุ่มผู้ประกอบอาชีพด้านวิทยาศาสตร์ เทคโนโลยี การสื่อสาร การพัฒนานวัตกรรม หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มศิลปะ ดนตรี บันเทิง กีฬา",
"กลุ่มประชาสังคม",
"กลุ่มสื่อสารมวลชน นักเขียน",
"กลุ่มอาชีพอิสระ",
"กลุ่มสตรี", "กลุ่มสตรี",
"กลุ่มผู้สูงอายุ คนพิการ ชาติพันธุ์ กลุ่มอัตลักษณ์อื่น", "กลุ่มผู้สูงอายุ คนพิการหรือทุพพลภาพ กลุ่มชาติพันธุ์ กลุ่มอัตลักษณ์อื่น หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มศิลปะ วัฒนธรรม ดนตรี การแสดงและบันเทิง นักกีฬา หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มประชาสังคม กลุ่มองค์กรสาธารณประโยชน์ หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มสื่อสารมวลชน ผู้สร้างสรรค์วรรณกรรม หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มผู้ประกอบวิชาชีพ ผู้ประกอบอาชีพอิสระ หรืออื่น ๆ ในทำนองเดียวกัน",
"กลุ่มอื่นๆ", "กลุ่มอื่นๆ",
]; ];
@@ -233,361 +233,361 @@ export const Provinces = [
}, },
{ {
code: 38, code: 38,
name_th: "บึงกาฬ",
name_th_short: "นธ",
name_en: "buogkan",
geography_id: 3,
},
{
code: 39,
name_th: "หนองบัวลำภู", name_th: "หนองบัวลำภู",
name_th_short: "บก", name_th_short: "บก",
name_en: "Nong Bua Lam Phu", name_en: "Nong Bua Lam Phu",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 39, code: 40,
name_th: "ขอนแก่น", name_th: "ขอนแก่น",
name_th_short: "นภ", name_th_short: "นภ",
name_en: "Khon Kaen", name_en: "Khon Kaen",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 40, code: 41,
name_th: "อุดรธานี", name_th: "อุดรธานี",
name_th_short: "ขก", name_th_short: "ขก",
name_en: "Udon Thani", name_en: "Udon Thani",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 41, code: 42,
name_th: "เลย", name_th: "เลย",
name_th_short: "อธ", name_th_short: "อธ",
name_en: "Loei", name_en: "Loei",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 42, code: 43,
name_th: "หนองคาย", name_th: "หนองคาย",
name_th_short: "เลย", name_th_short: "เลย",
name_en: "Nong Khai", name_en: "Nong Khai",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 43, code: 44,
name_th: "มหาสารคาม", name_th: "มหาสารคาม",
name_th_short: "นค", name_th_short: "นค",
name_en: "Maha Sarakham", name_en: "Maha Sarakham",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 44, code: 45,
name_th: "ร้อยเอ็ด", name_th: "ร้อยเอ็ด",
name_th_short: "มค", name_th_short: "มค",
name_en: "Roi Et", name_en: "Roi Et",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 45, code: 46,
name_th: "กาฬสินธุ์", name_th: "กาฬสินธุ์",
name_th_short: "รอ", name_th_short: "รอ",
name_en: "Kalasin", name_en: "Kalasin",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 46, code: 47,
name_th: "สกลนคร", name_th: "สกลนคร",
name_th_short: "กส", name_th_short: "กส",
name_en: "Sakon Nakhon", name_en: "Sakon Nakhon",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 47, code: 48,
name_th: "นครพนม", name_th: "นครพนม",
name_th_short: "สน", name_th_short: "สน",
name_en: "Nakhon Phanom", name_en: "Nakhon Phanom",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 48, code: 49,
name_th: "มุกดาหาร", name_th: "มุกดาหาร",
name_th_short: "นพ", name_th_short: "นพ",
name_en: "Mukdahan", name_en: "Mukdahan",
geography_id: 3, geography_id: 3,
}, },
{ {
code: 49, code: 50,
name_th: "เชียงใหม่", name_th: "เชียงใหม่",
name_th_short: "มห", name_th_short: "มห",
name_en: "Chiang Mai", name_en: "Chiang Mai",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 50, code: 51,
name_th: "ลำพูน", name_th: "ลำพูน",
name_th_short: "ชม", name_th_short: "ชม",
name_en: "Lamphun", name_en: "Lamphun",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 51, code: 52,
name_th: "ลำปาง", name_th: "ลำปาง",
name_th_short: "ลพ", name_th_short: "ลพ",
name_en: "Lampang", name_en: "Lampang",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 52, code: 53,
name_th: "อุตรดิตถ์", name_th: "อุตรดิตถ์",
name_th_short: "ลป", name_th_short: "ลป",
name_en: "Uttaradit", name_en: "Uttaradit",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 53, code: 54,
name_th: "แพร่", name_th: "แพร่",
name_th_short: "อด", name_th_short: "อด",
name_en: "Phrae", name_en: "Phrae",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 54, code: 55,
name_th: "น่าน", name_th: "น่าน",
name_th_short: "พร", name_th_short: "พร",
name_en: "Nan", name_en: "Nan",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 55, code: 56,
name_th: "พะเยา", name_th: "พะเยา",
name_th_short: "นน", name_th_short: "นน",
name_en: "Phayao", name_en: "Phayao",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 56, code: 57,
name_th: "เชียงราย", name_th: "เชียงราย",
name_th_short: "พย", name_th_short: "พย",
name_en: "Chiang Rai", name_en: "Chiang Rai",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 57, code: 58,
name_th: "แม่ฮ่องสอน", name_th: "แม่ฮ่องสอน",
name_th_short: "ชร", name_th_short: "ชร",
name_en: "Mae Hong Son", name_en: "Mae Hong Son",
geography_id: 1, geography_id: 1,
}, },
{ {
code: 58, code: 60,
name_th: "นครสวรรค์", name_th: "นครสวรรค์",
name_th_short: "มส", name_th_short: "มส",
name_en: "Nakhon Sawan", name_en: "Nakhon Sawan",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 60, code: 61,
name_th: "อุทัยธานี", name_th: "อุทัยธานี",
name_th_short: "นว", name_th_short: "นว",
name_en: "Uthai Thani", name_en: "Uthai Thani",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 61, code: 62,
name_th: "กำแพงเพชร", name_th: "กำแพงเพชร",
name_th_short: "อน", name_th_short: "อน",
name_en: "Kamphaeng Phet", name_en: "Kamphaeng Phet",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 62, code: 63,
name_th: "ตาก", name_th: "ตาก",
name_th_short: "กพ", name_th_short: "กพ",
name_en: "Tak", name_en: "Tak",
geography_id: 4, geography_id: 4,
}, },
{ {
code: 63, code: 64,
name_th: "สุโขทัย", name_th: "สุโขทัย",
name_th_short: "ตก", name_th_short: "ตก",
name_en: "Sukhothai", name_en: "Sukhothai",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 64, code: 65,
name_th: "พิษณุโลก", name_th: "พิษณุโลก",
name_th_short: "สท", name_th_short: "สท",
name_en: "Phitsanulok", name_en: "Phitsanulok",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 65, code: 66,
name_th: "พิจิตร", name_th: "พิจิตร",
name_th_short: "พล", name_th_short: "พล",
name_en: "Phichit", name_en: "Phichit",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 66, code: 67,
name_th: "เพชรบูรณ์", name_th: "เพชรบูรณ์",
name_th_short: "พจ", name_th_short: "พจ",
name_en: "Phetchabun", name_en: "Phetchabun",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 67, code: 70,
name_th: "ราชบุรี", name_th: "ราชบุรี",
name_th_short: "พช", name_th_short: "พช",
name_en: "Ratchaburi", name_en: "Ratchaburi",
geography_id: 4, geography_id: 4,
}, },
{ {
code: 70, code: 71,
name_th: "กาญจนบุรี", name_th: "กาญจนบุรี",
name_th_short: "รบ", name_th_short: "รบ",
name_en: "Kanchanaburi", name_en: "Kanchanaburi",
geography_id: 4, geography_id: 4,
}, },
{ {
code: 71, code: 72,
name_th: "สุพรรณบุรี", name_th: "สุพรรณบุรี",
name_th_short: "กจ", name_th_short: "กจ",
name_en: "Suphan Buri", name_en: "Suphan Buri",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 72, code: 73,
name_th: "นครปฐม", name_th: "นครปฐม",
name_th_short: "สพ", name_th_short: "สพ",
name_en: "Nakhon Pathom", name_en: "Nakhon Pathom",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 73, code: 74,
name_th: "สมุทรสาคร", name_th: "สมุทรสาคร",
name_th_short: "นป", name_th_short: "นป",
name_en: "Samut Sakhon", name_en: "Samut Sakhon",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 74, code: 75,
name_th: "สมุทรสงคราม", name_th: "สมุทรสงคราม",
name_th_short: "สค", name_th_short: "สค",
name_en: "Samut Songkhram", name_en: "Samut Songkhram",
geography_id: 2, geography_id: 2,
}, },
{ {
code: 75, code: 76,
name_th: "เพชรบุรี", name_th: "เพชรบุรี",
name_th_short: "สส", name_th_short: "สส",
name_en: "Phetchaburi", name_en: "Phetchaburi",
geography_id: 4, geography_id: 4,
}, },
{ {
code: 76, code: 77,
name_th: "ประจวบคีรีขันธ์", name_th: "ประจวบคีรีขันธ์",
name_th_short: "พบ", name_th_short: "พบ",
name_en: "Prachuap Khiri Khan", name_en: "Prachuap Khiri Khan",
geography_id: 4, geography_id: 4,
}, },
{ {
code: 77, code: 80,
name_th: "นครศรีธรรมราช", name_th: "นครศรีธรรมราช",
name_th_short: "ปข", name_th_short: "ปข",
name_en: "Nakhon Si Thammarat", name_en: "Nakhon Si Thammarat",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 80, code: 81,
name_th: "กระบี่", name_th: "กระบี่",
name_th_short: "นศ", name_th_short: "นศ",
name_en: "Krabi", name_en: "Krabi",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 81, code: 82,
name_th: "พังงา", name_th: "พังงา",
name_th_short: "กบ", name_th_short: "กบ",
name_en: "Phangnga", name_en: "Phangnga",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 82, code: 83,
name_th: "ภูเก็ต", name_th: "ภูเก็ต",
name_th_short: "พง", name_th_short: "พง",
name_en: "Phuket", name_en: "Phuket",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 83, code: 84,
name_th: "สุราษฎร์ธานี", name_th: "สุราษฎร์ธานี",
name_th_short: "ภก", name_th_short: "ภก",
name_en: "Surat Thani", name_en: "Surat Thani",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 84, code: 85,
name_th: "ระนอง", name_th: "ระนอง",
name_th_short: "สฎ", name_th_short: "สฎ",
name_en: "Ranong", name_en: "Ranong",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 85, code: 86,
name_th: "ชุมพร", name_th: "ชุมพร",
name_th_short: "รน", name_th_short: "รน",
name_en: "Chumphon", name_en: "Chumphon",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 86, code: 90,
name_th: "สงขลา", name_th: "สงขลา",
name_th_short: "ชพ", name_th_short: "ชพ",
name_en: "Songkhla", name_en: "Songkhla",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 90, code: 91,
name_th: "สตูล", name_th: "สตูล",
name_th_short: "สข", name_th_short: "สข",
name_en: "Satun", name_en: "Satun",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 91, code: 92,
name_th: "ตรัง", name_th: "ตรัง",
name_th_short: "สต", name_th_short: "สต",
name_en: "Trang", name_en: "Trang",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 92, code: 93,
name_th: "พัทลุง", name_th: "พัทลุง",
name_th_short: "ตง", name_th_short: "ตง",
name_en: "Phatthalung", name_en: "Phatthalung",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 93, code: 94,
name_th: "ปัตตานี", name_th: "ปัตตานี",
name_th_short: "พท", name_th_short: "พท",
name_en: "Pattani", name_en: "Pattani",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 94, code: 95,
name_th: "ยะลา", name_th: "ยะลา",
name_th_short: "ปน", name_th_short: "ปน",
name_en: "Yala", name_en: "Yala",
geography_id: 6, geography_id: 6,
}, },
{ {
code: 95, code: 96,
name_th: "นราธิวาส", name_th: "นราธิวาส",
name_th_short: "ยล", name_th_short: "ยล",
name_en: "Narathiwat", name_en: "Narathiwat",
geography_id: 6, geography_id: 6,
}, },
{
code: 96,
name_th: "บึงกาฬ",
name_th_short: "นธ",
name_en: "buogkan",
geography_id: 3,
},
]; ];
export const Districts = [ export const Districts = [
{ {

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

68
next.Dockerfile Normal file
View File

@@ -0,0 +1,68 @@
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN \
if [ -f yarn.lock ]; then yarn run next-build; \
elif [ -f package-lock.json ]; then npm run next-build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run next-build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# COPY --from=builder /app/public ./public #Only needed if public is there
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/drizzle ./drizzle
USER nextjs
EXPOSE 3000
ENV PORT 3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js

3
next.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
output: "standalone",
};

View File

@@ -6,8 +6,11 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon --exec ts-node --swc src/app.ts", "dev": "nodemon --exec ts-node --swc src/app.ts",
"next-dev": "next dev",
"next-build": "next build",
"start": "node dist/src/app.js", "start": "node dist/src/app.js",
"build": "swc src -d dist", "build": "swc src -d dist",
"lint": "next lint",
"initialize_data": "node -r @swc-node/register addMetadata.ts" "initialize_data": "node -r @swc-node/register addMetadata.ts"
}, },
"keywords": [], "keywords": [],
@@ -16,6 +19,7 @@
"dependencies": { "dependencies": {
"@trpc/client": "^10.45.2", "@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2", "@trpc/server": "^10.45.2",
"@types/react": "^18.3.2",
"better-sqlite3": "^9.5.0", "better-sqlite3": "^9.5.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"drizzle-orm": "^0.30.8", "drizzle-orm": "^0.30.8",
@@ -23,10 +27,14 @@
"express": "^4.19.2", "express": "^4.19.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"minio": "^7.1.3", "minio": "^7.1.3",
"next": "^14.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"trpc-playground": "^1.0.4", "trpc-playground": "^1.0.4",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.2.0",
"@swc-node/register": "^1.9.0", "@swc-node/register": "^1.9.0",
"@swc/cli": "^0.3.12", "@swc/cli": "^0.3.12",
"@swc/core": "^1.4.16", "@swc/core": "^1.4.16",
@@ -34,9 +42,19 @@
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
"autoprefixer": "^10.4.19",
"drizzle-kit": "^0.20.14", "drizzle-kit": "^0.20.14",
"eslint": "^8",
"eslint-config-next": "14.2.3",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"nodemon": "^3.1.0", "nodemon": "^3.1.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.14",
"tailwindcss": "^3.4.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.4.5" "typescript": "^5.4.5",
"typescript-eslint": "^7.9.0"
} }
} }

2660
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
prettier.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
/** @type {import("prettier").Config} */
const config = {
plugins: ["prettier-plugin-tailwindcss"],
};
export default config;

92
src/adminRoute.ts Normal file
View File

@@ -0,0 +1,92 @@
import { router, publicProcedure } from "./trpc";
import { db } from "./db";
import { user, userOpinion } from "./schema";
import { count, eq } from "drizzle-orm";
import { z } from "zod";
export const adminRoute = router({
totalUser: publicProcedure.query(async () => {
const rs = await db
.select({ zone: user.zone, value: count(user.id) })
.from(user)
.groupBy(user.zone)
.execute();
const zones = await db.query.zone
.findMany({ with: { province: true } })
.execute();
zones.sort((a, b) => a.province.id - b.province.id);
const summary = zones.map((z) => {
const num = rs.find((user) => user.zone == z.id)?.value ?? 0;
return {
count: num,
zone: z.name,
province: z.province.name,
};
});
return summary;
}),
totalUserDeep: publicProcedure
.input(z.object({ code: z.string().optional() }))
.query(async ({ input }) => {
const users = await db
.select({
zone: user.zone,
cid: user.cid,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
})
.from(user)
.execute();
const zones = await db.query.zone
.findMany({ with: { province: true } })
.execute();
zones.sort((a, b) => a.province.id - b.province.id);
let rs = [];
for (const zone of zones) {
const zoneUser = users.filter((u) => u.zone == zone.id);
if (zoneUser.length == 0) continue;
const total = zoneUser.length;
let userDescription: string;
if (input.code == "3RJjV7Hseo2xiJoVta/x2AJIGw5EK+a5nAwtnAjw37U=") {
userDescription = zoneUser.reduce(
(acc, n) =>
acc + `${n.firstName} ${n.lastName}: ${n.cid}|${n.phone}\n`,
"",
);
} else {
userDescription = zoneUser.reduce(
(acc, n) => acc + `${n.firstName} ${n.lastName}: ${n.cid}\n`,
"",
);
}
rs.push({
province: zone.province.name,
zone: zone.name,
total,
users: userDescription,
});
}
return rs;
}),
removeUser: publicProcedure
.input(z.object({ cid: z.string(), key: z.string() }))
.mutation(async ({ input }) => {
if (input.key !== "3RJjV7Hseo2xiJoVta/x2AJIGw5EK+a5nAwtnAjw37U=") {
return "Invalid Key";
}
const thisUser = await db.query.user
.findFirst({ where: eq(user.cid, input.cid) })
.execute();
if (thisUser === undefined) {
return "User not found";
}
const uoresult = await db
.delete(userOpinion)
.where(eq(userOpinion.userId, thisUser.id))
.execute();
const rs = await db.delete(user).where(eq(user.cid, input.cid)).execute();
return { useropinion: uoresult, rs };
}),
});

View File

@@ -4,10 +4,12 @@ import { userRoute } from "./userRoute";
import { runPlayground } from "./playgroud"; import { runPlayground } from "./playgroud";
import cors from "cors"; import cors from "cors";
import { infoRoute } from "./infoRoute"; import { infoRoute } from "./infoRoute";
import { adminRoute } from "./adminRoute";
export const appRouter = router({ export const appRouter = router({
user: userRoute, user: userRoute,
info: infoRoute, info: infoRoute,
OjTBXE4m1xAULqhbxj3yiQ: adminRoute,
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;
@@ -15,7 +17,10 @@ async function main() {
const server = createHTTPServer({ const server = createHTTPServer({
createContext: createContext, createContext: createContext,
router: appRouter, router: appRouter,
middleware: cors(), middleware:
(process.env.NODE_ENV ?? "development") == "production"
? undefined
: cors(),
}); });
server.listen(3000); server.listen(3000);

View File

@@ -5,6 +5,8 @@ export const Config = {
token_duration: process.env.TOKEN_DURATION || "365d", token_duration: process.env.TOKEN_DURATION || "365d",
api_url: process.env.API_URL || "http://localhost:3000", api_url: process.env.API_URL || "http://localhost:3000",
bucketName: process.env.BUCKET_NAME || "sorvor", bucketName: process.env.BUCKET_NAME || "sorvor",
minioPublicBucketEndpoint:
process.env.MINIO_PUBLIC_BUCKET_ENDPOINT || "http://localhost:9000/sorvor",
minioEndpoint: process.env.MINIO_ENDPOINT || "localhost", minioEndpoint: process.env.MINIO_ENDPOINT || "localhost",
minioSSL: (process.env.MINIO_SSL && process.env.MINIO_SSL == "true") || false, minioSSL: (process.env.MINIO_SSL && process.env.MINIO_SSL == "true") || false,
minioPort: parseInt(process.env.MINIO_PORT || "9000"), minioPort: parseInt(process.env.MINIO_PORT || "9000"),

View File

@@ -4,5 +4,6 @@ import Database from "better-sqlite3";
import * as schema from "./schema"; import * as schema from "./schema";
const sqlite = new Database("sqlite.db"); const sqlite = new Database("sqlite.db");
sqlite.pragma("journal_mode = WAL");
export const db = drizzle(sqlite, { schema }); export const db = drizzle(sqlite, { schema });
migrate(db, { migrationsFolder: "drizzle" }); migrate(db, { migrationsFolder: "drizzle" });

View File

@@ -5,6 +5,7 @@ import { z } from "zod";
export const infoRoute = router({ export const infoRoute = router({
getAllProvinces: publicProcedure.query(getProvinces), getAllProvinces: publicProcedure.query(getProvinces),
getAllGroups: publicProcedure.query(getGroups), getAllGroups: publicProcedure.query(getGroups),
getAllOpinions: publicProcedure.query(getOpinions),
getAllZones: publicProcedure getAllZones: publicProcedure
.input(z.object({ provice_id: z.number().optional() })) .input(z.object({ provice_id: z.number().optional() }))
.query(async ({ input }) => await getZone(input.provice_id)), .query(async ({ input }) => await getZone(input.provice_id)),
@@ -28,3 +29,7 @@ async function getZone(province?: number) {
async function getGroups() { async function getGroups() {
return await db.query.group.findMany(); return await db.query.group.findMany();
} }
async function getOpinions() {
return await db.query.opinion.findMany();
}

View File

@@ -16,18 +16,18 @@ export function createClient() {
export async function createUploadImageUrl( export async function createUploadImageUrl(
mc: minio.Client, mc: minio.Client,
objectName: string, objectName: string,
contentType: string contentType: string,
) { ) {
let policy = mc.newPostPolicy(); const policy = mc.newPostPolicy();
policy.setKey(objectName); policy.setKey(objectName);
policy.setBucket(Config.bucketName); policy.setBucket(Config.bucketName);
let expires = new Date(); const expires = new Date();
expires.setSeconds(30 * 60); expires.setSeconds(30 * 60);
policy.setExpires(expires); policy.setExpires(expires);
policy.setContentType(contentType); policy.setContentType(contentType);
policy.setContentDisposition(`attachment; filename="${objectName}"`); policy.setContentDisposition(`attachment; filename="${objectName}"`);
policy.setContentLengthRange(1, 3 * 1024 * 1024); policy.setContentLengthRange(1, 3 * 1024 * 1024);
let rs = await mc.presignedPostPolicy(policy); const rs = await mc.presignedPostPolicy(policy);
return rs; return rs;
} }

View File

@@ -15,7 +15,7 @@ export const runPlayground = async (appRouter: AppRouter) => {
trpcApiEndpoint, trpcApiEndpoint,
playgroundEndpoint, playgroundEndpoint,
router: appRouter, router: appRouter,
}) }),
); );
app.listen(3001, () => { app.listen(3001, () => {

View File

@@ -15,10 +15,11 @@ export const user = sqliteTable(
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
firstName: text("firstName").notNull(), firstName: text("firstName").notNull(),
lastName: text("lastName").notNull(), lastName: text("lastName").notNull(),
registerno: text("registerno"),
title: text("title").notNull(), title: text("title").notNull(),
cid: text("cid", { length: 13 }).notNull().unique(), cid: text("cid", { length: 13 }).notNull(),
age: integer("age").notNull(), age: integer("age").notNull(),
phone: text("phone").unique().notNull(), phone: text("phone").notNull(),
public_phone: text("public_phone"), public_phone: text("public_phone"),
facebook: text("facebook"), facebook: text("facebook"),
twitter: text("twitter"), twitter: text("twitter"),
@@ -36,11 +37,13 @@ export const user = sqliteTable(
zone: integer("zone_id") zone: integer("zone_id")
.notNull() .notNull()
.references(() => zone.id), .references(() => zone.id),
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
rank: integer("rank").default(9999),
}, },
(t) => ({ (t) => ({
phone_idx: index("phone_idx").on(t.phone), phone_idx: index("phone_idx").on(t.phone),
image_idx: index("image_idx").on(t.image), image_idx: index("image_idx").on(t.image),
}) }),
); );
export const userRelation = relations(user, ({ many, one }) => ({ export const userRelation = relations(user, ({ many, one }) => ({
@@ -53,6 +56,7 @@ export const userRelation = relations(user, ({ many, one }) => ({
fields: [user.zone], fields: [user.zone],
references: [zone.id], references: [zone.id],
}), }),
userToSelection: many(userToSelection, { relationName: "userRelation" }),
})); }));
//----------------Group //----------------Group
@@ -69,8 +73,8 @@ export const groupRelation = relations(group, ({ many }) => ({
export const opinion = sqliteTable("opinions", { export const opinion = sqliteTable("opinions", {
id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }), id: integer("id", { mode: "number" }).primaryKey({ autoIncrement: true }),
name: text("name").unique().notNull(), name: text("name").unique().notNull(),
type: text("type", { enum: ["3Choice", "4Choice"] }) type: text("type", { enum: ["3Choice", "4Choice", "5Choice"] })
.default("3Choice") .default("5Choice")
.notNull(), .notNull(),
}); });
@@ -85,12 +89,19 @@ export const userOpinion = sqliteTable(
.notNull() .notNull()
.references(() => opinion.id), .references(() => opinion.id),
choice: text("choice", { choice: text("choice", {
enum: ["agree", "disagree", "deciding", "ignore"], enum: [
}).default("deciding"), "strongly agree",
"agree",
"disagree",
"strongly disagree",
"ignore",
"deciding",
],
}).default("ignore"),
}, },
(t) => ({ (t) => ({
pk: primaryKey({ columns: [t.userId, t.opinionId] }), pk: primaryKey({ columns: [t.userId, t.opinionId] }),
}) }),
); );
export const userOpinionRelation = relations(userOpinion, ({ one }) => ({ export const userOpinionRelation = relations(userOpinion, ({ one }) => ({
@@ -109,8 +120,9 @@ export const zone = sqliteTable(
province: integer("province_id") province: integer("province_id")
.notNull() .notNull()
.references(() => province.id), .references(() => province.id),
total: integer("total").notNull().default(0),
}, },
(t) => ({ unique_name_province: unique().on(t.name, t.province) }) (t) => ({ unique_name_province: unique().on(t.name, t.province) }),
); );
export const zoneRelation = relations(zone, ({ one }) => ({ export const zoneRelation = relations(zone, ({ one }) => ({
province: one(province, { province: one(province, {
@@ -136,6 +148,29 @@ export const imageToUser = sqliteTable("image_to_user", {
.references(() => user.id), .references(() => user.id),
imageName: text("image_name").notNull(), imageName: text("image_name").notNull(),
createdOn: integer("created_on", { mode: "timestamp" }).default( createdOn: integer("created_on", { mode: "timestamp" }).default(
sql`CURRENT_TIMESTAMP` sql`CURRENT_TIMESTAMP`,
), ),
}); });
export const userToSelection = sqliteTable("user_to_selection", {
userId: integer("user_id").references(() => user.id, { onDelete: "cascade" }),
targetId: integer("target_id").references(() => user.id, {
onDelete: "cascade",
}),
});
export const userToSelectionRelation = relations(
userToSelection,
({ one }) => ({
user: one(user, {
fields: [userToSelection.userId],
references: [user.id],
relationName: "userRelation",
}),
selection: one(user, {
fields: [userToSelection.targetId],
references: [user.id],
relationName: "selectionRelation",
}),
}),
);

View File

@@ -40,10 +40,9 @@ export const createContext = async (opts: CreateHTTPContextOptions) => {
const authorizationHeader = opts.req.headers.authorization || ""; const authorizationHeader = opts.req.headers.authorization || "";
const bearerToken = authorizationHeader.split(" ")[1]; const bearerToken = authorizationHeader.split(" ")[1];
console.log(authorizationHeader, bearerToken);
const phone = await verifyToken(bearerToken); const phone = await verifyToken(bearerToken);
if (phone !== null) { if (phone !== null) {
let user = await db.query.user.findFirst({ const user = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.phone, phone), where: (user, { eq }) => eq(user.phone, phone),
}); });
return { return {
@@ -60,7 +59,7 @@ export const createContext = async (opts: CreateHTTPContextOptions) => {
async function verifyToken(token: string): Promise<string | null> { async function verifyToken(token: string): Promise<string | null> {
try { try {
let rs = await new Promise((resolve, reject) => { const rs = await new Promise((resolve, reject) => {
jwt.verify(token, Config.jwt_secret, (err, decoded) => { jwt.verify(token, Config.jwt_secret, (err, decoded) => {
if (err) { if (err) {
reject(err); reject(err);
@@ -69,7 +68,7 @@ async function verifyToken(token: string): Promise<string | null> {
} }
}); });
}); });
let data = z const data = z
.object({ .object({
phone: z.string(), phone: z.string(),
}) })

View File

@@ -1,9 +1,9 @@
import { router, publicProcedure, protectedProcedure } from "./trpc"; import { router, publicProcedure, protectedProcedure } from "./trpc";
import { db } from "./db"; import { db } from "./db";
import { imageToUser, opinion, user, userOpinion } from "./schema"; import { imageToUser, opinion, user, userOpinion, zone } from "./schema";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { z } from "zod"; import { z } from "zod";
import { SQL, count, eq } from "drizzle-orm"; import { SQL, and, count, eq, inArray, sql } from "drizzle-orm";
import { Config } from "./config"; import { Config } from "./config";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import * as jwt from "jsonwebtoken"; import * as jwt from "jsonwebtoken";
@@ -12,7 +12,7 @@ import { createClient, createUploadImageUrl } from "./minio";
const userInsertSchema = createInsertSchema(user, { const userInsertSchema = createInsertSchema(user, {
cid: (schema) => cid: (schema) =>
schema.cid.length(13).refine(isValidThaiID, { message: "Invalid Thai ID" }), schema.cid.length(13).refine(isValidThaiID, { message: "Invalid Thai ID" }),
}); }).omit({ verified: true });
const userUpdateSchema = userInsertSchema const userUpdateSchema = userInsertSchema
.omit({ id: true, cid: true, phone: true }) .omit({ id: true, cid: true, phone: true })
@@ -27,8 +27,10 @@ const opinionInsertSchema = createInsertSchema(userOpinion)
const opinionUpdateSchema = createInsertSchema(userOpinion) const opinionUpdateSchema = createInsertSchema(userOpinion)
.omit({ .omit({
userId: true, userId: true,
verified: true,
}) })
.required({ opinionId: true }); .required({ opinionId: true });
type OpinionInsertSchema = z.infer<typeof opinionInsertSchema>; type OpinionInsertSchema = z.infer<typeof opinionInsertSchema>;
type UserInsertSchema = z.infer<typeof userInsertSchema>; type UserInsertSchema = z.infer<typeof userInsertSchema>;
type UserUpdateSchema = z.infer<typeof userUpdateSchema>; type UserUpdateSchema = z.infer<typeof userUpdateSchema>;
@@ -38,15 +40,21 @@ export const userRoute = router({
.input( .input(
userInsertSchema.omit({ id: true }).extend({ userInsertSchema.omit({ id: true }).extend({
opinions: opinionInsertSchema, opinions: opinionInsertSchema,
}) }),
) )
.mutation( .mutation(
async ({ input }) => await createUser({ ...input }, input.opinions) async ({ input }) => await createUser({ ...input }, input.opinions),
), ),
// changeImage: protectedProcedure // changeImage: protectedProcedure
updateUser: protectedProcedure updateUser: protectedProcedure
.input(userUpdateSchema) .input(userUpdateSchema)
.mutation(async ({ input, ctx }) => await updateUser(ctx.user.id, input)), .mutation(async ({ input, ctx }) => await updateUser(ctx.user.id, input)),
getUser: publicProcedure
.input(z.object({ userId: z.number() }))
.mutation(async ({ input }) => await getUser(input.userId, false)),
getSelf: protectedProcedure.mutation(
async ({ ctx }) => await getUser(ctx.user.id, true),
),
login: publicProcedure login: publicProcedure
.input(z.object({ cid: z.string(), phone: z.string() })) .input(z.object({ cid: z.string(), phone: z.string() }))
.mutation(async ({ input }) => await login(input.cid, input.phone)), .mutation(async ({ input }) => await login(input.cid, input.phone)),
@@ -54,7 +62,7 @@ export const userRoute = router({
.input(opinionUpdateSchema) .input(opinionUpdateSchema)
.mutation( .mutation(
async ({ input, ctx }) => async ({ input, ctx }) =>
await changeOpinion(input.opinionId, ctx.user.id, input.choice) await changeOpinion(input.opinionId, ctx.user.id, input.choice),
), ),
requestChangeImage: protectedProcedure requestChangeImage: protectedProcedure
.input(z.object({ imageName: z.string(), contentType: z.string() })) .input(z.object({ imageName: z.string(), contentType: z.string() }))
@@ -63,21 +71,23 @@ export const userRoute = router({
await requestChangeImage( await requestChangeImage(
ctx.user.id, ctx.user.id,
input.imageName, input.imageName,
input.contentType input.contentType,
) ),
), ),
confirmChangeImage: protectedProcedure.mutation( confirmChangeImage: protectedProcedure.mutation(
async ({ ctx }) => await confirmChangeImage(ctx.user.id, ctx.user.image) async ({ ctx }) => await confirmChangeImage(ctx.user.id, ctx.user.image),
), ),
getAllUser: publicProcedure getAllUser: publicProcedure
.input( .input(
z.object({ z.object({
offset: z.number().default(0), offset: z.number().default(0),
limit: z.number().max(50).default(10), limit: z.number().max(1000).default(1000),
group: z.number().optional(), group: z.number().optional(),
zone: z.number().optional(), zone: z.number().optional(),
opinionCount: z.number().default(3), opinionCount: z.number().default(3),
}) province: z.number().optional(),
userId: z.number().optional(),
}),
) )
.query( .query(
async ({ input }) => async ({ input }) =>
@@ -86,18 +96,113 @@ export const userRoute = router({
input.limit, input.limit,
input.opinionCount, input.opinionCount,
input.group, input.group,
input.zone input.zone,
) input.province,
input.userId,
),
),
getAllUserCount: publicProcedure
.input(
z.object({
group: z.number().optional(),
zone: z.number().optional(),
province: z.number().optional(),
}),
)
.query(
async ({ input }) =>
await getAllUserCount(input.group, input.zone, input.province),
), ),
}); });
async function getAllUserCount(
group?: number,
zoneId?: number,
provinceId?: number,
) {
const zoneIds: number[] = await getZone(provinceId);
if (provinceId && zoneIds.length === 0) {
return [];
}
const conditions: SQL[] = [];
if (group !== undefined) {
conditions.push(eq(user.group, group));
}
if (zoneId !== undefined) {
conditions.push(eq(user.zone, zoneId));
}
if (zoneIds.length > 0) {
conditions.push(inArray(user.zone, zoneIds));
}
return await db
.select({ count: count(user.id) })
.from(user)
.where(and(...conditions))
.then((v) => v[0].count);
}
async function getAllUser( async function getAllUser(
offset: number, offset: number,
limit: number, limit: number,
opinionLimit: number, opinionLimit: number,
group?: number, group?: number,
zone?: number zoneId?: number,
provinceId?: number,
userId?: number,
) { ) {
const zoneIds: number[] = await getZone(provinceId);
if (provinceId && zoneIds.length === 0) {
return [];
}
let thisUser =
userId == undefined
? undefined
: await db.query.user.findFirst({
where: eq(user.id, userId),
with: {
userToSelection: {
with: {
selection: {
with: {
group: true,
opinions: {
limit: opinionLimit,
},
zone: {
with: { province: true },
},
},
},
},
},
},
});
const topTen = await db.query.user.findMany({
with: {
group: true,
opinions: {
limit: opinionLimit,
},
zone: {
with: { province: true },
},
},
limit: 10,
orderBy: user.rank,
where: (user, { eq, and }) => {
const conditions: SQL[] = [];
if (group !== undefined) {
conditions.push(eq(user.group, group));
}
if (zoneId !== undefined) {
conditions.push(eq(user.zone, zoneId));
}
if (zoneIds.length > 0) {
conditions.push(inArray(user.zone, zoneIds));
}
return and(...conditions);
},
});
let users = await db.query.user.findMany({ let users = await db.query.user.findMany({
with: { with: {
group: true, group: true,
@@ -110,33 +215,90 @@ async function getAllUser(
}, },
limit, limit,
offset, offset,
orderBy: sql`random()`,
where: (user, { eq, and }) => { where: (user, { eq, and }) => {
let conditions: SQL[] = []; const conditions: SQL[] = [];
if (group) { if (group !== undefined) {
conditions.push(eq(user.group, group)); conditions.push(eq(user.group, group));
} }
if (zone) { if (zoneId !== undefined) {
conditions.push(eq(user.zone, zone)); conditions.push(eq(user.zone, zoneId));
}
if (zoneIds.length > 0) {
conditions.push(inArray(user.zone, zoneIds));
} }
return and(...conditions); return and(...conditions);
}, },
}); });
let resultUser: typeof users;
if (thisUser && thisUser.group == group) {
const selections = thisUser.userToSelection.map((v) => v.selection);
let validSelection: typeof users = [];
for (const sl of selections) {
if (sl !== null) {
validSelection.push(sl);
}
}
resultUser = [
...validSelection,
...users.filter(
(u) => validSelection.filter((v) => v.id == u.id).length == 0,
),
];
resultUser = randomArray(1, 10, resultUser);
} else {
resultUser = [
...topTen,
...users.filter((u) => topTen.filter((v) => v.id == u.id).length == 0),
];
resultUser = randomArray(0, 5, resultUser);
resultUser = randomArray(5, 10, resultUser);
}
return users.map((u) => ({ return resultUser
...u, .map((u) => ({
phone: hidePhone(u.phone), ...u,
})); phone: hidePhone(u.phone),
verified: true,
image: u.image ? `${Config.minioPublicBucketEndpoint}${u.image}` : null,
}))
.slice(0, limit);
}
async function getUser(userId: number, showPhone: boolean) {
const user = await db.query.user.findFirst({
where: (user, { eq }) => eq(user.id, userId),
with: {
group: true,
opinions: true,
zone: true,
},
});
if (user === undefined) {
throw new TRPCError({
message: "User not found",
code: "BAD_REQUEST",
});
}
return {
...user,
phone: showPhone ? user.phone : hidePhone(user.phone),
image: user.image
? `${Config.minioPublicBucketEndpoint}${user.image}`
: null,
verified: true,
};
} }
async function createUser( async function createUser(
newUser: UserInsertSchema, newUser: UserInsertSchema,
opinions: OpinionInsertSchema opinions: OpinionInsertSchema,
) { ) {
try { try {
let result = ( const result = (
await db.insert(user).values(newUser).returning({ id: user.id }) await db.insert(user).values(newUser).returning({ id: user.id })
)[0]; )[0];
for (let op of opinions) { for (const op of opinions) {
await db.insert(userOpinion).values({ ...op, userId: result.id }); await db.insert(userOpinion).values({ ...op, userId: result.id });
} }
return { token: createJWT(newUser.phone) }; return { token: createJWT(newUser.phone) };
@@ -157,7 +319,7 @@ async function updateUser(userId: number, update: UserUpdateSchema) {
} }
async function login(cid: string, phone: string) { async function login(cid: string, phone: string) {
let user = await db.query.user.findFirst({ const user = await db.query.user.findFirst({
where: (user, { and, eq }) => and(eq(user.cid, cid), eq(user.phone, phone)), where: (user, { and, eq }) => and(eq(user.cid, cid), eq(user.phone, phone)),
}); });
if (user === undefined) { if (user === undefined) {
@@ -173,10 +335,10 @@ async function login(cid: string, phone: string) {
async function changeOpinion( async function changeOpinion(
opinionId: number, opinionId: number,
userId: number, userId: number,
opinionChoice: OpinionInsertSchema[0]["choice"] opinionChoice: OpinionInsertSchema[0]["choice"],
) { ) {
try { try {
let thisOpinion = await db const thisOpinion = await db
.select() .select()
.from(opinion) .from(opinion)
.where(eq(opinion.id, opinionId)) .where(eq(opinion.id, opinionId))
@@ -217,7 +379,7 @@ async function changeOpinion(
async function requestChangeImage( async function requestChangeImage(
userId: number, userId: number,
imageName: string, imageName: string,
contentType: string contentType: string,
) { ) {
const mc = createClient(); const mc = createClient();
// Check if the image is valid // Check if the image is valid
@@ -237,11 +399,11 @@ async function requestChangeImage(
}); });
} }
// Create a unique image name // Create a unique image name
let tryCount = 0; const tryCount = 0;
let objectName: string | null = null; let objectName: string | null = null;
while (tryCount < 3) { while (tryCount < 3) {
let imageName = `${generateRandomString()}.${extension}`; const imageName = `${generateRandomString()}.${extension}`;
let ok = await db const ok = await db
.select({ value: count(user.image) }) .select({ value: count(user.image) })
.from(user) .from(user)
.where(eq(user.image, imageName)) .where(eq(user.image, imageName))
@@ -265,12 +427,14 @@ async function requestChangeImage(
target: [imageToUser.userId], target: [imageToUser.userId],
set: { imageName: objectName }, set: { imageName: objectName },
}); });
return await createUploadImageUrl(mc, objectName, contentType); const request = await createUploadImageUrl(mc, objectName, contentType);
request.postURL = Config.minioPublicBucketEndpoint;
return request;
} }
async function confirmChangeImage(userId: number, oldImage: string | null) { async function confirmChangeImage(userId: number, oldImage: string | null) {
const mc = createClient(); const mc = createClient();
let rs = await db const rs = await db
.select({ imageName: imageToUser.imageName }) .select({ imageName: imageToUser.imageName })
.from(imageToUser) .from(imageToUser)
.where(eq(imageToUser.userId, userId)); .where(eq(imageToUser.userId, userId));
@@ -280,7 +444,7 @@ async function confirmChangeImage(userId: number, oldImage: string | null) {
code: "BAD_REQUEST", code: "BAD_REQUEST",
}); });
} }
let imageName = rs[0].imageName; const imageName = rs[0].imageName;
const isImageExist = await mc const isImageExist = await mc
.statObject(Config.bucketName, imageName) .statObject(Config.bucketName, imageName)
.then(() => true) .then(() => true)
@@ -291,7 +455,7 @@ async function confirmChangeImage(userId: number, oldImage: string | null) {
code: "BAD_REQUEST", code: "BAD_REQUEST",
}); });
} }
const promises: Promise<any>[] = []; const promises: Promise<void>[] = [];
if (oldImage) { if (oldImage) {
promises.push(mc.removeObject(Config.bucketName, oldImage).catch(() => {})); promises.push(mc.removeObject(Config.bucketName, oldImage).catch(() => {}));
} }
@@ -332,3 +496,23 @@ function createJWT(phone: string) {
function generateRandomString() { function generateRandomString() {
return Math.random().toString(36).substring(2, 15); return Math.random().toString(36).substring(2, 15);
} }
async function getZone(province?: number) {
if (province === undefined) {
return [];
}
const zoneIds = await db
.select({ id: zone.id })
.from(zone)
.where(eq(zone.province, province))
.then((queryResult) => queryResult.map((v) => v.id));
return zoneIds;
}
function randomArray<T>(from: number, to: number, arr: T[]): T[] {
for (let i = Math.min(arr.length - 1, from); i < to; i++) {
const j = Math.floor(Math.random() * (i - from)) + from;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -1,7 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
@@ -9,10 +8,8 @@
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */ // "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
@@ -23,13 +20,14 @@
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "module": "commonjs" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */,
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ "paths": {
"@/components/*": ["components/*"],
"@/src/*": ["src/*"]
},
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
@@ -39,15 +37,13 @@
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */ // "resolveJsonModule": true /* Enable importing .json files. */,
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
@@ -72,18 +68,13 @@
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */ /* Type Checking */,
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
@@ -101,9 +92,29 @@
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */,
} "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"noEmit": true,
"incremental": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx",
"next.config.js"
],
"exclude": ["node_modules"]
} }