not even sure
This commit is contained in:
@@ -39,7 +39,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.63.0",
|
||||||
"react-image-crop": "^11.0.10",
|
"react-image-crop": "^11.0.10",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
|
@@ -18,8 +18,7 @@ const Profile = async () => {
|
|||||||
<AvatarUpload preloadedUser={preloadedUser} />
|
<AvatarUpload preloadedUser={preloadedUser} />
|
||||||
<Separator />
|
<Separator />
|
||||||
<UserInfoForm preloadedUser={preloadedUser} />
|
<UserInfoForm preloadedUser={preloadedUser} />
|
||||||
<Separator />
|
<ResetPasswordForm preloadedUser={preloadedUser} />
|
||||||
<ResetPasswordForm />
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<SignOutForm />
|
<SignOutForm />
|
||||||
</Card>
|
</Card>
|
||||||
|
@@ -222,16 +222,22 @@ const SignIn = () => {
|
|||||||
</SubmitButton>
|
</SubmitButton>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
<div className='flex my-auto justify-center w-2/3'>
|
<div className='flex justify-center'>
|
||||||
<div className='flex flex-row w-1/3 items-center my-2.5'>
|
<div
|
||||||
|
className='flex flex-row items-center
|
||||||
|
my-2.5 mx-auto justify-center w-1/4'
|
||||||
|
>
|
||||||
<Separator className='py-0.5 mr-3' />
|
<Separator className='py-0.5 mr-3' />
|
||||||
<span className='font-semibold text-lg'>or</span>
|
<span className='font-semibold text-lg'>or</span>
|
||||||
<Separator className='py-0.5 ml-3' />
|
<Separator className='py-0.5 ml-3' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex my-auto justify-center'>
|
<div className='flex justify-center mb-3'>
|
||||||
<MicrosoftSignInButton />
|
<MicrosoftSignInButton />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex justify-center mt-3'>
|
||||||
|
<GibsAuthSignInButton />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -337,9 +343,12 @@ const SignIn = () => {
|
|||||||
<Separator className='py-0.5 ml-3' />
|
<Separator className='py-0.5 ml-3' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex my-auto justify-center'>
|
<div className='flex justify-center mb-3'>
|
||||||
<MicrosoftSignInButton type='signUp' />
|
<MicrosoftSignInButton type='signUp' />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex justify-center mt-3'>
|
||||||
|
<GibsAuthSignInButton type='signUp' />
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
@@ -20,7 +20,7 @@ export const MicrosoftSignInButton = ({
|
|||||||
<Button
|
<Button
|
||||||
size='lg'
|
size='lg'
|
||||||
onClick={() => signIn('microsoft-entra-id')}
|
onClick={() => signIn('microsoft-entra-id')}
|
||||||
className='text-lg font-semibold mx-auto'
|
className='text-lg font-semibold'
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row my-auto space-x-2'>
|
<div className='flex flex-row my-auto space-x-2'>
|
||||||
|
@@ -3,6 +3,7 @@ import { useState } from 'react';
|
|||||||
import { useAction } from 'convex/react';
|
import { useAction } from 'convex/react';
|
||||||
import { api } from '~/convex/_generated/api';
|
import { api } from '~/convex/_generated/api';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { type Preloaded, usePreloadedQuery } from 'convex/react';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
Input,
|
Input,
|
||||||
|
Separator,
|
||||||
SubmitButton,
|
SubmitButton,
|
||||||
} from '@/components/ui';
|
} from '@/components/ui';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -62,7 +64,12 @@ const formSchema = z
|
|||||||
path: ['confirmPassword'],
|
path: ['confirmPassword'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ResetPasswordForm = () => {
|
type ResetFormProps = {
|
||||||
|
preloadedUser: Preloaded<typeof api.auth.getUser>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ResetPasswordForm = ({ preloadedUser }: ResetFormProps) => {
|
||||||
|
const user = usePreloadedQuery(preloadedUser);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const changePassword = useAction(api.auth.updateUserPassword);
|
const changePassword = useAction(api.auth.updateUserPassword);
|
||||||
@@ -94,9 +101,11 @@ export const ResetPasswordForm = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
return user?.provider !== 'password' ? (
|
||||||
return (
|
<div />
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<Separator />
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className='text-2xl'>Change Password</CardTitle>
|
<CardTitle className='text-2xl'>Change Password</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
@@ -133,7 +133,10 @@ export const UserInfoForm = ({ preloadedUser }: UserInfoFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Email</FormLabel>
|
<FormLabel>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input
|
||||||
|
{...field}
|
||||||
|
disabled={user?.provider !== 'password'}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Your email address associated with your account.
|
Your email address associated with your account.
|
||||||
|
10
bun.lock
10
bun.lock
@@ -5,7 +5,7 @@
|
|||||||
"name": "techtracker",
|
"name": "techtracker",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.19.17",
|
"@types/node": "^20.19.17",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.36.0",
|
||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"turbo": "^2.5.6",
|
"turbo": "^2.5.6",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"radix-ui": "^1.4.3",
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.63.0",
|
||||||
"react-image-crop": "^11.0.10",
|
"react-image-crop": "^11.0.10",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
@@ -106,7 +106,9 @@
|
|||||||
"name": "@techtracker/convex",
|
"name": "@techtracker/convex",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"convex": "^1.27.0",
|
"convex": "^1.27.0",
|
||||||
|
"resend": "^6.1.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
@@ -2347,7 +2349,7 @@
|
|||||||
|
|
||||||
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
|
"react-freeze": ["react-freeze@1.0.4", "", { "peerDependencies": { "react": ">=17.0.0" } }, "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA=="],
|
||||||
|
|
||||||
"react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="],
|
"react-hook-form": ["react-hook-form@7.63.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA=="],
|
||||||
|
|
||||||
"react-image-crop": ["react-image-crop@11.0.10", "", { "peerDependencies": { "react": ">=16.13.1" } }, "sha512-+5FfDXUgYLLqBh1Y/uQhIycpHCbXkI50a+nbfkB1C0xXXUTwkisHDo2QCB1SQJyHCqIuia4FeyReqXuMDKWQTQ=="],
|
"react-image-crop": ["react-image-crop@11.0.10", "", { "peerDependencies": { "react": ">=16.13.1" } }, "sha512-+5FfDXUgYLLqBh1Y/uQhIycpHCbXkI50a+nbfkB1C0xXXUTwkisHDo2QCB1SQJyHCqIuia4FeyReqXuMDKWQTQ=="],
|
||||||
|
|
||||||
@@ -2407,6 +2409,8 @@
|
|||||||
|
|
||||||
"requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="],
|
"requireg": ["requireg@0.2.2", "", { "dependencies": { "nested-error-stacks": "~2.0.1", "rc": "~1.2.7", "resolve": "~1.7.1" } }, "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg=="],
|
||||||
|
|
||||||
|
"resend": ["resend@6.1.0", "", { "peerDependencies": { "@react-email/render": "^1.1.0" }, "optionalPeers": ["@react-email/render"] }, "sha512-H0cJI2pcLk5/dGwyvZUHu+O7X/q6arvc40EWm+pRPuy+PSWojH5utZtmDBUZ2L0+gVwYZiWs6y2lw6GQA1z1rg=="],
|
||||||
|
|
||||||
"resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="],
|
"resolve": ["resolve@1.22.8", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
@@ -10,9 +10,12 @@ import { type Id } from './_generated/dataModel';
|
|||||||
import { action, mutation, query } from './_generated/server';
|
import { action, mutation, query } from './_generated/server';
|
||||||
import Authentik from '@auth/core/providers/authentik';
|
import Authentik from '@auth/core/providers/authentik';
|
||||||
import { Entra, Password, validatePassword } from './custom/auth';
|
import { Entra, Password, validatePassword } from './custom/auth';
|
||||||
|
import Resend from '@auth/core/providers/resend';
|
||||||
|
import { Resend as ResendAPI } from 'resend';
|
||||||
|
import { generateRandomString, RandomReader } from '@oslojs/crypto/random';
|
||||||
|
|
||||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||||
providers: [Password, Authentik, Entra],
|
providers: [Authentik, Entra, Password],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const PASSWORD_MIN = 8;
|
export const PASSWORD_MIN = 8;
|
||||||
@@ -172,3 +175,32 @@ export const updateUserPassword = action({
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//export const ResendOTPPasswordReset = Resend({
|
||||||
|
//id: "resend-otp",
|
||||||
|
//apiKey: process.env.AUTH_RESEND_KEY,
|
||||||
|
//async generateVerificationToken() {
|
||||||
|
//const random: RandomReader = {
|
||||||
|
//read(bytes) {
|
||||||
|
//crypto.getRandomValues(bytes);
|
||||||
|
//},
|
||||||
|
//};
|
||||||
|
|
||||||
|
//const alphabet = "0123456789";
|
||||||
|
//const length = 8;
|
||||||
|
//return generateRandomString(random, alphabet, length);
|
||||||
|
//},
|
||||||
|
//async sendVerificationRequest({ identifier: email, provider, token }) {
|
||||||
|
//const resend = new ResendAPI(provider.apiKey);
|
||||||
|
//const { error } = await resend.emails.send({
|
||||||
|
//from: "My App <onboarding@resend.dev>",
|
||||||
|
//to: [email],
|
||||||
|
//subject: `Reset your password in My App`,
|
||||||
|
//text: "Your password reset code is " + token,
|
||||||
|
//});
|
||||||
|
|
||||||
|
//if (error) {
|
||||||
|
//throw new Error("Could not send");
|
||||||
|
//}
|
||||||
|
//},
|
||||||
|
//});
|
||||||
|
@@ -58,29 +58,33 @@ export const create = mutation({
|
|||||||
updatedBy: v.optional(v.id('users')),
|
updatedBy: v.optional(v.id('users')),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const authUserId = await getAuthUserId(ctx);
|
const authUserId: Id<'users'> | null = await getAuthUserId(ctx);
|
||||||
if (!args.userId && !authUserId) {
|
if (!args.userId && !authUserId) {
|
||||||
throw new ConvexError('Not authenticated.');
|
throw new ConvexError('Not authenticated.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = args.userId ?? authUserId!;
|
|
||||||
const updatedBy = args.updatedBy ?? authUserId!;
|
|
||||||
|
|
||||||
await ensureUser(ctx, userId);
|
|
||||||
await ensureUser(ctx, updatedBy);
|
|
||||||
|
|
||||||
const message = args.message.trim();
|
const message = args.message.trim();
|
||||||
if (message.length === 0) {
|
if (message.length === 0) {
|
||||||
throw new ConvexError('Message cannot be empty.');
|
throw new ConvexError('Message cannot be empty.');
|
||||||
}
|
}
|
||||||
|
const userId = args.userId ?? authUserId!;
|
||||||
const statusId = await ctx.db.insert('statuses', {
|
const updatedBy = args.updatedBy ?? authUserId;
|
||||||
message,
|
await ensureUser(ctx, userId);
|
||||||
userId,
|
let statusId: Id<'statuses'>;
|
||||||
updatedBy,
|
if (updatedBy) {
|
||||||
updatedAt: Date.now(),
|
ensureUser(ctx, updatedBy);
|
||||||
});
|
statusId = await ctx.db.insert('statuses', {
|
||||||
|
message,
|
||||||
|
userId,
|
||||||
|
updatedBy,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
statusId = await ctx.db.insert('statuses', {
|
||||||
|
message,
|
||||||
|
userId,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
await ctx.db.patch(userId, { currentStatusId: statusId });
|
await ctx.db.patch(userId, { currentStatusId: statusId });
|
||||||
return { statusId };
|
return { statusId };
|
||||||
},
|
},
|
||||||
@@ -125,7 +129,6 @@ export const bulkCreate = mutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update all users - simplified
|
|
||||||
export const updateAllStatuses = mutation({
|
export const updateAllStatuses = mutation({
|
||||||
args: { message: v.string() },
|
args: { message: v.string() },
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
@@ -152,22 +155,19 @@ export const updateAllStatuses = mutation({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Lunch status with automatic return - this should be an action
|
|
||||||
export const createLunchStatus = mutation({
|
export const createLunchStatus = mutation({
|
||||||
args: {},
|
args: {},
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const authUserId = await getAuthUserId(ctx);
|
const authUserId = await getAuthUserId(ctx);
|
||||||
if (!authUserId) throw new ConvexError('Not authenticated.');
|
if (!authUserId) throw new ConvexError('Not authenticated.');
|
||||||
// Create lunch status
|
|
||||||
await ctx.runMutation(api.statuses.create, {
|
await ctx.runMutation(api.statuses.create, {
|
||||||
message: 'At lunch',
|
message: 'At lunch',
|
||||||
userId: authUserId
|
userId: authUserId,
|
||||||
});
|
});
|
||||||
const oneHour = 60 * 60 * 1000;
|
const oneHour = 60 * 60 * 1000;
|
||||||
console.log('Scheduling return to desk after 1 hour');
|
|
||||||
await ctx.scheduler.runAfter(oneHour, api.statuses.create, {
|
await ctx.scheduler.runAfter(oneHour, api.statuses.create, {
|
||||||
message: 'At desk',
|
message: 'At desk',
|
||||||
userId: authUserId
|
userId: authUserId,
|
||||||
});
|
});
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
@@ -257,7 +257,10 @@ export const listHistory = query({
|
|||||||
userId: v.optional(v.id('users')),
|
userId: v.optional(v.id('users')),
|
||||||
paginationOpts: paginationOptsValidator,
|
paginationOpts: paginationOptsValidator,
|
||||||
},
|
},
|
||||||
handler: async (ctx, { userId, paginationOpts }): Promise<Paginated<StatusRow>> => {
|
handler: async (
|
||||||
|
ctx,
|
||||||
|
{ userId, paginationOpts },
|
||||||
|
): Promise<Paginated<StatusRow>> => {
|
||||||
const result = userId
|
const result = userId
|
||||||
? await ctx.db
|
? await ctx.db
|
||||||
.query('statuses')
|
.query('statuses')
|
||||||
|
@@ -11,7 +11,9 @@
|
|||||||
"author": "Gib",
|
"author": "Gib",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"convex": "^1.27.0"
|
"@oslojs/crypto": "^1.0.1",
|
||||||
|
"convex": "^1.27.0",
|
||||||
|
"resend": "^6.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.2"
|
||||||
|
Reference in New Issue
Block a user