Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
DATABASE_URL="file:${PWD}/prisma/dev.db"
NUXT_AUTH0_BASE_URL='http://localhost:3000'
NUXT_AUTH0_ISSUER_BASE_URL='https://march-to-the-polls.us.auth0.com'
NUXT_AUTH0_CLIENT_ID='DxkcUTlJbv9MKRGG7I7SEliHMl8ufN5a'
NUXT_AUTH0_CLIENT_SECRET='EXAMPLE_CLIENT_SECRET'
DATABASE_URL="file:./dev.db"
BETTER_AUTH_SECRET="replace-with-a-32-char-secret"
BETTER_AUTH_URL="http://localhost:3000"
SEED_ADMIN_EMAIL="admin@example.org"

# SMTP configuration for sending OTP emails
SMTP_HOST="smtp.example.com"
SMTP_PORT=587
SMTP_USER="your-email@example.com"
SMTP_PASS="your-email-password"
SMTP_FROM="March to the Polls <noreply@example.com>"
1 change: 0 additions & 1 deletion dockerfile → Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ COPY --from=builder /app/package.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/pnpm-workspace.yaml ./
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/cert-dev.pem ./
# Commented out because MTTP is a Prisma 6 app
# COPY --from=builder /app/prisma.config.ts ./
RUN npm i -g pnpm
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Central page with MTTP logo to welcome you to the website


## Third party integrations
# Auth0
Takes care of all user authentication to signup, login, and logout of the webpage
# BetterAuth
Handles user authentication via email OTP (one-time password) for login

## Tech Stack
# Frameworks
Expand Down
44 changes: 20 additions & 24 deletions app.vue
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
<template>
<PageHeader :userRole="userRole"/>
<NuxtPage ></NuxtPage>
</template>


<script setup lang="ts">

const runtime = useRuntimeConfig()
const router = useRouter()
const routes = ref(router.getRoutes())
const route = useRoute()
const cvCookie = useCookie('cvtoken')
const cvuser = useCookie('cvuser')


const userRole = computed(() => (cvuser.value.role));

if(!cvCookie.value)
{
navigateTo('/api/login')
}

</script>
<template>
<PageHeader v-if="loggedIn" :userRole="userRole"/>
<NuxtPage ></NuxtPage>
</template>


<script setup lang="ts">

import { authClient } from "~~/lib/auth-client";

const route = useRoute();
const { data: session } = await authClient.useSession(useFetch);
const loggedIn = computed(() => !!session.value);
const userRole = computed(() => session.value?.user?.role);

if (!import.meta.server && !loggedIn.value && route.path !== "/login") {
navigateTo("/login");
}

</script>
19 changes: 0 additions & 19 deletions cert-dev.pem

This file was deleted.

9 changes: 8 additions & 1 deletion components/PageHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<li><nuxt-link to="/database" v-if="userRole === 'staff' || userRole === 'admin'" class="navPage">View
Students</nuxt-link></li>
<li><nuxt-link to="/admin" v-if="userRole === 'admin'" class="navPage">Admin</nuxt-link></li>
<li><a class="right navPage" href="/api/logout">Logout</a></li>
<li><a class="right navPage" href="#" @click.prevent="handleLogout">Logout</a></li>
</ul>
</div>
<nav>
Expand All @@ -35,7 +35,14 @@
</template>

<script setup>
import { authClient } from "~~/lib/auth-client";

const props = defineProps(['userRole']);

async function handleLogout() {
await authClient.signOut();
navigateTo("/login");
}
</script>


Expand Down
13 changes: 13 additions & 0 deletions lib/auth-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createAuthClient } from "better-auth/vue";
import { emailOTPClient } from "better-auth/client/plugins";
import { inferAdditionalFields } from "better-auth/client/plugins";
import type { auth } from "./auth";

export const authClient = createAuthClient({
plugins: [
emailOTPClient(),
inferAdditionalFields<typeof auth>(),
],
});

export const { signIn, signOut, useSession } = authClient;
64 changes: 64 additions & 0 deletions lib/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { emailOTP } from "better-auth/plugins";
import { PrismaClient } from "@/prisma/client/client";
import nodemailer from "nodemailer";

const prisma = new PrismaClient();

const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT) || 587,
secure: Number(process.env.SMTP_PORT) === 465,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});

export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "sqlite",
}),
user: {
additionalFields: {
firstName: { type: "string", required: true },
lastName: { type: "string", required: true },
role: {
type: "string",
required: true,
defaultValue: "volunteer",
input: false,
},
},
},
plugins: [
emailOTP({
disableSignUp: true,
async sendVerificationOTP({ email, otp, type }) {
if (type === "sign-in") {
await transporter.sendMail({
from: process.env.SMTP_FROM || process.env.SMTP_USER,
to: email,
subject: "Your March to the Polls Login Code",
text: `Your verification code is: ${otp}`,
html: `
<div style="font-family: sans-serif; max-width: 400px; margin: 0 auto; padding: 20px;">
<h2 style="color: #122C4F; text-align: center;">March to the Polls</h2>
<p>Your verification code is:</p>
<p style="font-size: 32px; font-weight: bold; text-align: center; letter-spacing: 8px; color: #122C4F; background: #f3f4f6; padding: 16px; border-radius: 8px;">${otp}</p>
<p style="color: #666; font-size: 14px;">This code expires in 5 minutes. If you did not request this code, you can safely ignore this email.</p>
</div>
`,
});
}
},
}),
],
session: {
expiresIn: 60 * 60 * 24 * 7,
updateAge: 60 * 60 * 24,
},
});

export type Session = typeof auth.$Infer.Session;
77 changes: 47 additions & 30 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,47 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
css: [
'~/src/assets/fonts.css',
],
head: {
link: [
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@500&family=Poppins&display=swap',
},
],
},
modules: ['@nuxtjs/tailwindcss'],
runtimeConfig: {
AUTH0_CLIENT_ID: "",
AUTH0_CLIENT_SECRET: "",
AUTH0_BASE_URL: "",
AUTH0_ISSUER_BASE_URL: "",
AUTH0_PUB_KEY_PATH: './cert-dev.pem',
},
build: {
transpile: ['@vuepic/vue-datepicker']
},
/* nitro: {
replace: {
'typeof window': '`undefined`',
},
} */
})
import tailwindcss from "@tailwindcss/vite";

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',

srcDir: '.',

css: [
'~/src/assets/tailwind.css',
'~/src/assets/fonts.css',
],

app: {
head: {
link: [
{
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Open+Sans:wght@500&family=Poppins&display=swap',
},
],
},
},

modules: [],

vite: {
plugins: [tailwindcss()],
optimizeDeps: {
include: [
"better-auth/client/plugins",
"better-auth/vue"
]
}
},

runtimeConfig: {
BETTER_AUTH_SECRET: "",
BETTER_AUTH_URL: "http://localhost:3000",
SMTP_HOST: "",
SMTP_PORT: 587,
SMTP_USER: "",
SMTP_PASS: "",
SMTP_FROM: "",
},

})
29 changes: 14 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "nuxt-app",
"name": "mttp",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -11,27 +11,26 @@
},
"devDependencies": {
"@prisma/client": "^6.6.0",
"prisma": "^6.6.0"
"@types/nodemailer": "^8.0.1",
"prisma": "^6.6.0",
"tsx": "^4.23.1"
},
"description": "Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.",
"description": "March to the Polls - Student Voter Registration Portal",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"nuxt": "3.13.1",
"@nuxt/ui-templates": "^1.3.1",
"@nuxtjs/tailwindcss": "^6.8.0",
"@types/node": "^20.8.7",
"autoprefixer": "^10.4.16",
"jose": "^6.0.10",
"nuxt-headlessui": "^1.1.4",
"@better-auth/prisma-adapter": "^1.6.25",
"@tailwindcss/vite": "^4.3.3",
"better-auth": "^1.6.25",
"nodemailer": "^9.0.3",
"nuxt": "4.5.0",
"papaparse": "^5.5.2",
"postcss": "^8.4.31",
"pug": "^3.0.2",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.1.4",
"tailwindcss": "^4.3.3",
"xlsx": "^0.18.5"
},
"prisma": {
"seed": "pnpm tsx prisma/seed.ts"
},
"keywords": [],
"author": "",
"license": "ISC"
Expand Down
Loading
Loading