feat: refactor auth into singleton and improve UI with custom fonts/colors/animations
This commit is contained in:
parent
345ee6a5e6
commit
cb10fd657f
@ -1,94 +1,5 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { compare } from "bcryptjs";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
adapter: PrismaAdapter(prisma) as any,
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
||||
authorization: {
|
||||
params: {
|
||||
prompt: "consent",
|
||||
access_type: "offline",
|
||||
response_type: "code"
|
||||
}
|
||||
}
|
||||
}),
|
||||
// Apple Sign In can be added when credentials are available
|
||||
// AppleProvider({
|
||||
// clientId: process.env.APPLE_ID || "",
|
||||
// clientSecret: process.env.APPLE_SECRET || "",
|
||||
// }),
|
||||
CredentialsProvider({
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials.email
|
||||
}
|
||||
});
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const isPasswordValid = await compare(
|
||||
credentials.password,
|
||||
user.passwordHash
|
||||
);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name || null,
|
||||
image: user.image || null,
|
||||
};
|
||||
}
|
||||
})
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt"
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
signOut: "/auth/login",
|
||||
error: "/auth/login",
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
(session.user as any).id = token.id;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET
|
||||
};
|
||||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "@/lib/auth";
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { createCalendarEvent, updateCalendarEvent, deleteCalendarEvent, CalendarConnection } from '@/lib/calendar-events';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { google } from 'googleapis';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { google } from 'googleapis';
|
||||
|
||||
// Initiate Google Calendar OAuth flow
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getTokens, getUserCalendars } from '@/lib/outlook-calendar';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { getAuthUrl } from '@/lib/outlook-calendar';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '../../auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { getCalendarEvents, CalendarConnection } from '@/lib/calendar-events';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '../auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { PrismaClient, Task } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
import { authOptions } from "@/lib/auth";
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '../../auth/[...nextauth]/route';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
|
||||
|
||||
@ -654,7 +654,7 @@ h3 {
|
||||
.weekly-day-date {
|
||||
font-size: var(--weekly-date-size, 0.65rem);
|
||||
font-weight: var(--weekly-date-weight, 400);
|
||||
color: var(--weekly-text-light);
|
||||
color: var(--weekly-date-color, var(--weekly-text-light));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 0.25rem;
|
||||
@ -664,9 +664,8 @@ h3 {
|
||||
.weekly-day-name {
|
||||
font-size: var(--weekly-headline-size, 1.25rem);
|
||||
font-weight: var(--weekly-headline-weight, 900);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--weekly-text);
|
||||
color: var(--weekly-weekday-color, var(--weekly-text));
|
||||
font-family: var(--weekly-font-headline) !important;
|
||||
}
|
||||
|
||||
@ -717,7 +716,7 @@ h3 {
|
||||
font-size: var(--weekly-task-size, 0.9rem);
|
||||
font-weight: var(--weekly-task-weight, 400);
|
||||
line-height: var(--weekly-task-line-height, 1.5);
|
||||
color: var(--weekly-text);
|
||||
color: var(--weekly-task-color, var(--weekly-text));
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
@ -1612,9 +1611,9 @@ h3 {
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
font-family: var(--weekly-font);
|
||||
font-weight: var(--weekly-event-weight, 600);
|
||||
font-size: var(--weekly-event-size, 0.85rem);
|
||||
font-family: var(--weekly-event-font, var(--weekly-font));
|
||||
}
|
||||
|
||||
.time-slot-event .event-title {
|
||||
@ -1622,14 +1621,15 @@ h3 {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
font-family: var(--weekly-font);
|
||||
font-weight: var(--weekly-event-weight, 500);
|
||||
font-family: var(--weekly-event-font, var(--weekly-font));
|
||||
font-size: var(--weekly-event-size, 0.85rem);
|
||||
}
|
||||
|
||||
.time-slot-event .event-time-row {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.9;
|
||||
font-family: var(--weekly-font);
|
||||
font-family: var(--weekly-event-font, var(--weekly-font));
|
||||
}
|
||||
|
||||
.time-slot-event:hover {
|
||||
@ -2274,28 +2274,42 @@ h3 {
|
||||
|
||||
/* --- VIEW TRANSITION ANIMATION (Weekly-like) --- */
|
||||
|
||||
/* Only apply Container transitions (Full Slide) when navType is 'week' */
|
||||
[data-nav-type="week"] .weekly-days-grid {
|
||||
view-transition-name: week-grid;
|
||||
}
|
||||
|
||||
/* When navType="day", NO name on container, so columns animate individually via inline view-transition-name */
|
||||
|
||||
/* Base Slide Animations */
|
||||
@keyframes slideOutToLeft {
|
||||
to {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
to { transform: translateX(-20%); opacity: 0; }
|
||||
}
|
||||
@keyframes slideInFromRight {
|
||||
from { transform: translateX(20%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
@keyframes slideOutToRight {
|
||||
to { transform: translateX(20%); opacity: 0; }
|
||||
}
|
||||
@keyframes slideInFromLeft {
|
||||
from { transform: translateX(-20%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
/* Week Grid Transition Group */
|
||||
::view-transition-group(week-grid) {
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
/* Slide Next (Left) */
|
||||
[data-slide-direction="next"]::view-transition-old(week-grid) {
|
||||
animation-name: slideOutToLeft;
|
||||
}
|
||||
[data-slide-direction="next"]::view-transition-new(week-grid) {
|
||||
animation-name: slideInFromRight;
|
||||
}
|
||||
|
||||
/* Slide Prev (Right) */
|
||||
[data-slide-direction="prev"]::view-transition-old(week-grid) {
|
||||
animation-name: slideOutToRight;
|
||||
}
|
||||
[data-slide-direction="prev"]::view-transition-new(week-grid) {
|
||||
animation-name: slideInFromLeft;
|
||||
}
|
||||
|
||||
@keyframes slideOutToRight {
|
||||
|
||||
@ -450,7 +450,7 @@ export default function WeeklyView() {
|
||||
const [cellDuration, setCellDuration] = useState<CellDuration>(60);
|
||||
const [draggedTask, setDraggedTask] = useState<Task | null>(null);
|
||||
const [showTimeGrid, setShowTimeGrid] = useState(true);
|
||||
const [slideDirection, setSlideDirection] = useState<'left' | 'right' | 'out-left' | 'out-right' | 'in-left' | 'in-right' | null>(null);
|
||||
const [slideDirection, setSlideDirection] = useState<'next' | 'prev' | null>(null);
|
||||
const [activeSlot, setActiveSlot] = useState<{ day: number; slot: string } | null>(null);
|
||||
const [newSlotTask, setNewSlotTask] = useState('');
|
||||
const [selectedTaskForNotes, setSelectedTaskForNotes] = useState<Task | null>(null);
|
||||
@ -1149,12 +1149,13 @@ export default function WeeklyView() {
|
||||
const navigate = (newDate: Date, direction: 'left' | 'right', type: 'day' | 'week') => {
|
||||
if (typeof document !== 'undefined' && 'startViewTransition' in document) {
|
||||
const doc = document as any;
|
||||
doc.documentElement.dataset.transitionDirection = direction === 'left' ? 'next' : 'prev';
|
||||
doc.documentElement.dataset.slideDirection = direction === 'left' ? 'next' : 'prev';
|
||||
doc.documentElement.dataset.navType = 'week'; // Always use full-grid slide for both day and week
|
||||
|
||||
doc.startViewTransition(() => {
|
||||
setCurrentWeekStart(newDate);
|
||||
setSlideDirection(null);
|
||||
// setSlideDirection state is not strictly needed for global transition but keeping it clean
|
||||
setSlideDirection(direction === 'left' ? 'next' : 'prev');
|
||||
});
|
||||
} else {
|
||||
setCurrentWeekStart(newDate);
|
||||
@ -1987,8 +1988,10 @@ export default function WeeklyView() {
|
||||
|
||||
{/* Day Columns */}
|
||||
<main
|
||||
className={`weekly-days-grid cols-${viewDays} ${slideDirection ? `slide-${slideDirection}` : ''}`}
|
||||
style={{ viewTransitionName: 'week-grid' } as React.CSSProperties}
|
||||
className={`weekly-days-grid cols-${viewDays}`}
|
||||
data-slide-direction={slideDirection}
|
||||
data-nav-type={viewDays > 1 ? 'week' : 'day'}
|
||||
style={{ viewTransitionName: viewDays > 1 ? 'week-grid' : 'none' } as React.CSSProperties}
|
||||
>
|
||||
{getVisibleDays().map((date, colIndex) => (
|
||||
<div
|
||||
|
||||
117
src/lib/auth.ts
117
src/lib/auth.ts
@ -1,39 +1,84 @@
|
||||
// Mock authentication utilities
|
||||
import { NextAuthOptions } from "next-auth";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { compare } from "bcryptjs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export interface UserSession {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
export const authOptions: NextAuthOptions = {
|
||||
adapter: PrismaAdapter(prisma) as any,
|
||||
providers: [
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
||||
authorization: {
|
||||
params: {
|
||||
prompt: "consent",
|
||||
access_type: "offline",
|
||||
response_type: "code"
|
||||
}
|
||||
}
|
||||
}),
|
||||
CredentialsProvider({
|
||||
name: "credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify authentication token and return user session or null
|
||||
*/
|
||||
export async function verifyAuth(): Promise<UserSession | null> {
|
||||
// In a real implementation, this would decode and validate the JWT token
|
||||
// This is a simplified mock implementation
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null;
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Mock verification - in reality this would validate the JWT signature
|
||||
// and check expiration etc.
|
||||
return {
|
||||
id: 'mock-user-id',
|
||||
email: 'user@example.com'
|
||||
};
|
||||
}
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: credentials.email
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Require authentication - throw error if no valid session, return session if valid
|
||||
*/
|
||||
export async function requireAuth(): Promise<UserSession> {
|
||||
const session = await verifyAuth();
|
||||
|
||||
if (!session) {
|
||||
throw new Error('Authentication required');
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
if (!user || !user.passwordHash) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
const isPasswordValid = await compare(
|
||||
credentials.password,
|
||||
user.passwordHash
|
||||
);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
throw new Error("Invalid credentials");
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name || null,
|
||||
image: user.image || null,
|
||||
};
|
||||
}
|
||||
})
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt"
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
signOut: "/auth/login",
|
||||
error: "/auth/login",
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
(session.user as any).id = token.id;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user