Brijesh's Git Server — k3yst0n3 @ 9a5241c6a2a1c4480fa0bb43c3b4a5689d48c0f8

feat(auth): add basic authentication

# Changes Made
- Create basic JWT auth
- both api endpoints and ui

# Details
Only includes logic for authentication, still need to add middleware to validate jwt and authorise based on role
Brijesh brijesh@wawdhane.com
Tue, 14 May 2024 01:41:36 +0530
commit

9a5241c6a2a1c4480fa0bb43c3b4a5689d48c0f8

parent

90706b0412aaed46082b4bb5e78238a3f1630d06

M .air.toml.air.toml

@@ -15,7 +15,7 @@ full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"

# Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "html"] # Ignore these filename extensions or directories. -exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"] +exclude_dir = ["client", "tmp", "vendor", "frontend/node_modules"] # Watch these directories if you specified. include_dir = [] # Exclude files.
D .idea/.gitignore

@@ -1,8 +0,0 @@

-# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml
D .idea/k3yst0n3.iml

@@ -1,9 +0,0 @@

-<?xml version="1.0" encoding="UTF-8"?> -<module type="WEB_MODULE" version="4"> - <component name="Go" enabled="true" /> - <component name="NewModuleRootManager"> - <content url="file://$MODULE_DIR$" /> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - </component> -</module>
D .idea/modules.xml

@@ -1,8 +0,0 @@

-<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectModuleManager"> - <modules> - <module fileurl="file://$PROJECT_DIR$/.idea/k3yst0n3.iml" filepath="$PROJECT_DIR$/.idea/k3yst0n3.iml" /> - </modules> - </component> -</project>
D .idea/vcs.xml

@@ -1,6 +0,0 @@

-<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="VcsDirectoryMappings"> - <mapping directory="" vcs="Git" /> - </component> -</project>
A client/.eslintrc.json

@@ -0,0 +1,3 @@

+{ + "extends": "next/core-web-vitals" +}
A client/.gitignore

@@ -0,0 +1,36 @@

+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts
A client/README.md

@@ -0,0 +1,40 @@

+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
A client/helpers/cookie.ts

@@ -0,0 +1,26 @@

+function SetCookie(cname: string, cvalue: string, exdays: any): void { + const d = new Date(); + d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); + let expires = "expires=" + d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function GetCookie(cookieName: string): string | undefined { + if (typeof document !== undefined) { + let name = cookieName + "="; + let decodedCookie = decodeURIComponent(document.cookie); + let ca = decodedCookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + } + return undefined; +} + +export { SetCookie, GetCookie };
A client/helpers/jwt.ts

@@ -0,0 +1,10 @@

+function ParseJWT(token: string) { + if (!token || !token.split(".")[1]) { + return; + } + const base64Url = token.split(".")[1]; + const base64 = base64Url.replace("-", "+").replace("_", "/"); + return JSON.parse(window.atob(base64)); +} + +export default ParseJWT;
A client/next.config.mjs

@@ -0,0 +1,6 @@

+/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, +}; + +export default nextConfig;
A client/package.json

@@ -0,0 +1,28 @@

+{ + "name": "client", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "formik": "^2.4.6", + "next": "14.2.3", + "react": "^18", + "react-dom": "^18", + "react-hook-form": "^7.51.4" + }, + "devDependencies": { + "typescript": "^5", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "postcss": "^8", + "tailwindcss": "^3.4.1", + "eslint": "^8", + "eslint-config-next": "14.2.3" + } +}
A client/pages/_app.tsx

@@ -0,0 +1,6 @@

+import "@/styles/globals.css"; +import type { AppProps } from "next/app"; + +export default function App({ Component, pageProps }: AppProps) { + return <Component {...pageProps} />; +}
A client/pages/_document.tsx

@@ -0,0 +1,13 @@

+import { Html, Head, Main, NextScript } from "next/document"; + +export default function Document() { + return ( + <Html lang="en"> + <Head /> + <body> + <Main /> + <NextScript /> + </body> + </Html> + ); +}
A client/pages/api/hello.ts

@@ -0,0 +1,13 @@

+// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; + +type Data = { + name: string; +}; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse<Data>, +) { + res.status(200).json({ name: "John Doe" }); +}
A client/pages/index.tsx

@@ -0,0 +1,39 @@

+import { GetCookie, SetCookie } from "@/helpers/cookie"; +import ParseJWT from "@/helpers/jwt"; +import Link from "next/link"; +import React from "react"; + +function IndexPage() { + const [tokenData, setTokenData] = React.useState<any>() + + React.useEffect(() => { + setTokenData(ParseJWT(GetCookie("token") || "{}")) + }, []); + + return ( + <div className="flex flex-col gap-1 m-4"> + <h1>K3YST0N3</h1> + {tokenData + ? <div> + <pre>{JSON.stringify(tokenData, null, 2)}</pre> + <button className="border border-gray-400 rounded px-2 py-1" onClick={() => { + SetCookie("token", "", -1) + setTokenData(undefined) + }}>Logout</button> + </div> + : <> + <Link + href="/login" + className="text-blue-600 underline" + >Login</Link> + <Link + href="/register" + className="text-blue-600 underline" + >Register</Link> + </> + } + </div> + ); +} + +export default IndexPage;
A client/pages/login.tsx

@@ -0,0 +1,78 @@

+import { SetCookie } from '@/helpers/cookie'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { useForm, SubmitHandler } from "react-hook-form" + +type Inputs = { + email: string + password: string +} + +function LoginPage() { + const router = useRouter() + + const [response, setResponse] = React.useState<any>() + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<Inputs>() + const onSubmit: SubmitHandler<Inputs> = (data) => { + console.log(data) + + fetch("http://localhost:4000/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + }) + .then((res) => res.json()) + .then((data) => { + console.log(data) + setResponse(data) + if (data.message === "User logged in successfully") { + SetCookie("token", data.token, 86400) + router.push("/") + } + }) + .catch((error) => { + console.error("Error:", error) + setResponse(error) + }) + } + + return ( + <div className="flex flex-col gap-1 m-4"> + <h1>Login</h1> + <form onSubmit={handleSubmit(onSubmit)} className="border border-gray-400 rounded w-[370px] p-[10px]"> + <div className='flex flex-col w-[350px] mb-1'> + <label>Email</label> + <input + {...register("email", { required: true })} type="email" + className="border border-gray-400 px-2 py-1 rounded" + /> + {errors.email && <span>This field is required</span>} + </div> + <div className='flex flex-col w-[350px] mb-3'> + <label>Password</label> + <input + {...register("password", { required: true })} type="password" + className="border border-gray-400 px-2 py-1 rounded" + /> + {errors.password && <span>This field is required</span>} + </div> + {response && response.error && <div className="my-2 text-red-600">{response.error}</div>} + <button className="border border-gray-400 px-2 py-1 hover:cursor-pointer hover:bg-gray-100" type="submit">Submit</button> + </form> + <div className="mt-2 flex gap-1"> + Don't have an account? + <Link href="/register" className="text-blue-600 underline">Register</Link> + </div> + </div> + ); +} + +export default LoginPage;
A client/pages/register.tsx

@@ -0,0 +1,96 @@

+import { SetCookie } from '@/helpers/cookie'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { useForm, SubmitHandler } from "react-hook-form" + +type Inputs = { + first_name: string + last_name: string + email: string + password: string +} + +function RegistrationPage() { + const router = useRouter() + + const [response, setResponse] = React.useState<any>() + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm<Inputs>() + const onSubmit: SubmitHandler<Inputs> = (data) => { + console.log(data) + + fetch("http://localhost:4000/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data) + }) + .then((res) => res.json()) + .then((data) => { + console.log(data) + setResponse(data) + if (data.message === "User created successfully") { + SetCookie("token", data.token, 86400) + router.push("/") + } + }) + .catch((error) => { + console.error("Error:", error) + setResponse(error) + }) + } + + return ( + <div className="flex flex-col gap-1 m-4"> + <h1>Register</h1> + <form onSubmit={handleSubmit(onSubmit)} className="border border-gray-400 rounded w-[370px] p-[10px]"> + <div className='flex flex-col w-[350px] mb-1 -mt-[5px]'> + <label>First Name</label> + <input + {...register("first_name", { required: true })} + className="border border-gray-400 px-2 py-1 rounded" + /> + {errors.first_name && <span>This field is required</span>} + </div> + <div className='flex flex-col w-[350px] mb-1'> + <label>Last Name</label> + <input + {...register("last_name", { required: true })} + className="border border-gray-400 px-2 py-1 rounded" + /> + {errors.last_name && <span>This field is required</span>} + </div> + <div className='flex flex-col w-[350px] mb-1'> + <label>Email</label> + <input + {...register("email", { required: true })} type="email" + className="border border-gray-400 px-2 py-1 rounded" + /> + {errors.email && <span>This field is required</span>} + </div> + <div className='flex flex-col w-[350px] mb-3'> + <label>Password</label> + <input + {...register("password", { required: true })} type="password" + className="border border-gray-400 px-2 py-1 rounded" + /> + {errors.password && <span>This field is required</span>} + </div> + {response && response.error && <div className="my-2 text-red-600">{response.error}</div>} + <button className="border border-gray-400 px-2 py-1 hover:cursor-pointer hover:bg-gray-100" type="submit">Submit</button> + </form> + <div className="mt-2 flex gap-1"> + Already have an account? + <Link href="/login" className="text-blue-600 underline">Login</Link> + </div> + </div> + ); +} + +export default RegistrationPage;
A client/postcss.config.mjs

@@ -0,0 +1,8 @@

+/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config;
A client/public/next.svg

@@ -0,0 +1,1 @@

+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
A client/public/vercel.svg

@@ -0,0 +1,1 @@

+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
A client/styles/globals.css

@@ -0,0 +1,3 @@

+@tailwind base; +@tailwind components; +@tailwind utilities;
A client/tailwind.config.ts

@@ -0,0 +1,20 @@

+import type { Config } from "tailwindcss"; + +const config: Config = { + content: [ + "./pages/**/*.{js,ts,jsx,tsx,mdx}", + "./components/**/*.{js,ts,jsx,tsx,mdx}", + "./app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + backgroundImage: { + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", + }, + }, + }, + plugins: [], +}; +export default config;
A client/tsconfig.json

@@ -0,0 +1,21 @@

+{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}
M database/database.godatabase/database.go

@@ -2,6 +2,7 @@ package database

import ( "fmt" + "k3yst0n3/helpers" "k3yst0n3/models" "log" "os"

@@ -11,19 +12,19 @@ "gorm.io/gorm"

"gorm.io/gorm/logger" ) -type Dbinstance struct { - Db *gorm.DB +type DbinstanceType struct { + DB *gorm.DB } -var DB Dbinstance +var DBInstance DbinstanceType func ConnectDb() { dsn := fmt.Sprintf( - "host=postgres user=%s password=%s dbname=%s port=5432 sslmode=disable TimeZone=Asia/Shanghai", - "test", "test", "test", - // os.Getenv("DB_USER"), - // os.Getenv("DB_PASSWORD"), - // os.Getenv("DB_NAME"), + // "host=postgres user=%s password=%s dbname=%s port=5432 sslmode=disable TimeZone=Asia/Shanghai", + "host=postgres user=%s password=%s dbname=%s port=5432 sslmode=disable TimeZone=UTC", + helpers.GetEnv("POSTGRES_USER"), + helpers.GetEnv("POSTGRES_PASSWORD"), + helpers.GetEnv("POSTGRES_DB"), ) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{

@@ -38,9 +39,11 @@ log.Println("connected")

db.Logger = logger.Default.LogMode(logger.Info) log.Println("running migrations") - db.AutoMigrate(&models.Fact{}) + db.AutoMigrate(&models.User{}) + db.AutoMigrate(&models.Application{}) + db.AutoMigrate(&models.Event{}) - DB = Dbinstance{ - Db: db, + DBInstance = DbinstanceType{ + DB: db, } }
M docker-compose.yamldocker-compose.yaml

@@ -21,9 +21,9 @@ - minio-volume:/data

postgres: image: postgres:alpine environment: - - POSTGRES_USER=test - - POSTGRES_PASSWORD=test - - POSTGRES_DB=test + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} ports: - "5432:5432" volumes:
M go.modgo.mod

@@ -9,12 +9,16 @@ gorm.io/gorm v1.25.10

) require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect

@@ -25,4 +29,5 @@ golang.org/x/net v0.24.0 // indirect

golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.5.0 // indirect )
M go.sumgo.sum

@@ -1,6 +1,12 @@

github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=

@@ -13,6 +19,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=

github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=

@@ -45,6 +53,8 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=

golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
D handlers/facts.go

@@ -1,23 +0,0 @@

-package handlers - -import ( - "k3yst0n3/database" - "k3yst0n3/models" - - "github.com/labstack/echo/v4" -) - -func ListFacts(c echo.Context) error { - facts := []models.Fact{} - database.DB.Db.Find(&facts) - return c.JSON(200, facts) -} - -func CreateFact(c echo.Context) error { - fact := new(models.Fact) - if err := c.Bind(fact); err != nil { - return c.JSON(500, map[string]string{"message": err.Error()}) - } - database.DB.Db.Create(&fact) - return c.JSON(200, fact) -}
M handlers/health_check.goserver/health_check.go

@@ -1,4 +1,4 @@

-package handlers +package main import ( "net/http"
A helpers/get_env.go

@@ -0,0 +1,17 @@

+package helpers + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +func GetEnv(name string) string { + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + return os.Getenv(name) +}
A helpers/password-hash.go

@@ -0,0 +1,13 @@

+package helpers + +import "golang.org/x/crypto/bcrypt" + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +}
A helpers/uuid.go

@@ -0,0 +1,9 @@

+package helpers + +import ( + "github.com/google/uuid" +) + +func GenerateUUID() string { + return uuid.New().String() +}
A internal/auth/login.go

@@ -0,0 +1,73 @@

+package auth + +import ( + "k3yst0n3/database" + "k3yst0n3/helpers" + "k3yst0n3/models" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" +) + +func LoginUser(c echo.Context) error { + requestID := c.Response().Header().Get(echo.HeaderXRequestID) + user := new(models.User) + if err := c.Bind(user); err != nil { + return c.JSON(500, map[string]string{ + "error": "error binding user from request: " + err.Error(), + "request_id": requestID, + }) + } + necessaryFields := map[string]interface{}{ + "email": user.Email, + "password": user.Password, + } + for key, field := range necessaryFields { + if field == "" { + return c.JSON(400, map[string]string{ + "error": key + " is required", + "request_id": requestID, + }) + } + } + var existingUser models.User + database.DBInstance.DB.Where("email = ?", user.Email).First(&existingUser) + if existingUser.Email != user.Email { + return c.JSON(400, map[string]string{ + "error": "User does not exist", + "request_id": requestID, + }) + } + if !helpers.CheckPasswordHash(user.Password, existingUser.Password) { + return c.JSON(400, map[string]string{ + "error": "Invalid password", + "request_id": requestID, + }) + } + claims := &JwtCustomClaims{ + ID: existingUser.ID, + FirstName: existingUser.FirstName, + LastName: existingUser.LastName, + Role: existingUser.Role, + Email: existingUser.Email, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "k3yst0n3", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + encodedToken, err := token.SignedString([]byte(helpers.GetEnv("JWT_SIGNING_KEY"))) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Error while generating token", + "request_id": requestID, + }) + } + return c.JSON(http.StatusOK, map[string]string{ + "message": "User logged in successfully", + "token": encodedToken, + "request_id": requestID, + }) +}
A internal/auth/register.go

@@ -0,0 +1,88 @@

+package auth + +import ( + "k3yst0n3/database" + "k3yst0n3/helpers" + "k3yst0n3/models" + "net/http" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" +) + +func RegisterUser(c echo.Context) error { + requestID := c.Response().Header().Get(echo.HeaderXRequestID) + + user := new(models.User) + if err := c.Bind(user); err != nil { + return c.JSON(500, map[string]string{ + "error": "error binding user from request: " + err.Error(), + "request_id": requestID, + }) + } + + necessaryFields := map[string]interface{}{ + "first_name": user.FirstName, + "email": user.Email, + "password": user.Password, + } + + for key, field := range necessaryFields { + if field == "" { + return c.JSON(400, map[string]string{ + "error": key + " is required", + "request_id": requestID, + }) + } + } + + var existingUser models.User + database.DBInstance.DB.Where("email = ?", user.Email).First(&existingUser) + if existingUser.Email == user.Email { + return c.JSON(400, map[string]string{ + "error": "User already exists", + "request_id": requestID, + }) + } + + hashedPassword, err := helpers.HashPassword(user.Password) + if err != nil { + return c.JSON(500, map[string]string{ + "error": "error hashing password: " + err.Error(), + "request_id": requestID, + }) + } + + user.Password = hashedPassword + + database.DBInstance.DB.Create(&user) + + claims := &JwtCustomClaims{ + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Role: user.Role, + Email: user.Email, + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: "k3yst0n3", + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + encodedToken, err := token.SignedString([]byte(helpers.GetEnv("JWT_SIGNING_KEY"))) + if err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "Error while generating token", + "request_id": requestID, + }) + } + + return c.JSON(http.StatusOK, map[string]string{ + "message": "User created successfully", + "token": encodedToken, + "request_id": requestID, + }) +}
A internal/auth/type.go

@@ -0,0 +1,12 @@

+package auth + +import "github.com/golang-jwt/jwt/v5" + +type JwtCustomClaims struct { + jwt.RegisteredClaims + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Role string `json:"role"` + Email string `json:"email"` + ID uint `json:"id"` +}
M models/models.gomodels/models.go

@@ -1,9 +1,43 @@

package models -import "gorm.io/gorm" +import ( + "github.com/golang-jwt/jwt/v5" + "gorm.io/gorm" +) + +type JwtCustomClaims struct { + jwt.RegisteredClaims + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Role string `json:"role"` + Email string `json:"email"` + ID uint `json:"id"` +} -type Fact struct { +type User struct { gorm.Model - Question string `json:"question" gorm:"text;not null;default:null` - Answer string `json:"answer" gorm:"text;not null;default:null` + FirstName string `json:"first_name" gorm:"text"` + LastName string `json:"last_name" gorm:"text"` + Email string `json:"email" gorm:"text"` + Password string `json:"password" gorm:"text"` + Role string `json:"role" gorm:"text"` + Applications []Application `json:"applications" gorm:"foreignKey:UserID"` +} + +type Application struct { + gorm.Model + UserID string `json:"user_id" gorm:"text"` + Name string `json:"name" gorm:"text"` + Description string `json:"description" gorm:"text"` + Domain string `json:"domain" gorm:"text"` + Status string `json:"status" gorm:"text"` + TrackingCode string `json:"tracking_code" gorm:"text"` + Events []Event `json:"events" gorm:"foreignKey:ApplicationID"` +} + +type Event struct { + gorm.Model + ApplicationID string `json:"application_id" gorm:"text"` + Name string `json:"name" gorm:"text"` + Description string `json:"description" gorm:"text"` }
M server/main.goserver/main.go

@@ -4,12 +4,17 @@ import (

"k3yst0n3/database" "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) func main() { database.ConnectDb() e := echo.New() + + e.Use(middleware.Logger()) + e.Use(middleware.CORS()) + e.Use(middleware.RequestID()) setupRoutes(e)
M server/routes.goserver/routes.go

@@ -1,15 +1,15 @@

package main import ( - "k3yst0n3/handlers" + "k3yst0n3/internal/auth" "github.com/labstack/echo/v4" ) func setupRoutes(e *echo.Echo) { - e.GET("/health-check", handlers.HealthCheckHandler) - - e.GET("/facts", handlers.ListFacts) + e.GET("/health-check", HealthCheckHandler) - e.POST("/fact", handlers.CreateFact) + // authentication routes + e.POST("/register", auth.RegisterUser) + e.POST("/login", auth.LoginUser) }