@cogito.ai/cli 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/templates/web-nextjs/apps/docs/.source/server.ts +3 -3
  3. package/dist/templates/web-nextjs/apps/web/messages/en.json +51 -1
  4. package/dist/templates/web-nextjs/apps/web/messages/zh.json +51 -1
  5. package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
  6. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +130 -0
  7. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/login/page.tsx +7 -1
  8. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +112 -0
  9. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +9 -0
  10. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/page.tsx +9 -0
  11. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/profile/page.tsx +97 -0
  12. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/about/page.tsx +25 -0
  13. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/help/page.tsx +25 -0
  14. package/dist/templates/web-nextjs/apps/web/src/app/[locale]/privacy/page.tsx +12 -10
  15. package/dist/templates/web-nextjs/apps/web/src/app/auth/callback/route.ts +8 -2
  16. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/app-sidebar.tsx +27 -124
  17. package/dist/templates/web-nextjs/apps/web/src/components/dashboard/nav-user.tsx +6 -13
  18. package/dist/templates/web-nextjs/apps/web/src/components/profile/profile-form.tsx +64 -0
  19. package/dist/templates/web-nextjs/apps/web/src/core/repositories/IAuthRepository.ts +3 -0
  20. package/dist/templates/web-nextjs/apps/web/src/features/auth/__contract__.ts +6 -0
  21. package/dist/templates/web-nextjs/apps/web/src/features/auth/actions.ts +70 -1
  22. package/dist/templates/web-nextjs/apps/web/src/features/auth/index.ts +10 -1
  23. package/dist/templates/web-nextjs/apps/web/src/features/auth/server.ts +3 -0
  24. package/dist/templates/web-nextjs/apps/web/src/infra/db/SupabaseAuthRepository.ts +25 -0
  25. package/dist/templates/web-nextjs/apps/web/src/lib/validations/auth.ts +21 -0
  26. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -868,7 +868,7 @@ var package_default;
868
868
  var init_package = __esm(() => {
869
869
  package_default = {
870
870
  name: "@cogito.ai/cli",
871
- version: "0.4.1",
871
+ version: "0.4.2",
872
872
  type: "module",
873
873
  description: "AgentDock CLI – scaffold projects for humans and AI agents",
874
874
  publishConfig: {
@@ -6,8 +6,8 @@ import * as __fd_glob_7 from "../content/docs/decisions/turbo-package-manager.md
6
6
  import * as __fd_glob_6 from "../content/docs/changelog/index.mdx?collection=docs"
7
7
  import * as __fd_glob_5 from "../content/docs/index.mdx?collection=docs"
8
8
  import { default as __fd_glob_4 } from "../content/docs/roadmap/meta.json?collection=docs"
9
- import { default as __fd_glob_3 } from "../content/docs/features/meta.json?collection=docs"
10
- import { default as __fd_glob_2 } from "../content/docs/decisions/meta.json?collection=docs"
9
+ import { default as __fd_glob_3 } from "../content/docs/decisions/meta.json?collection=docs"
10
+ import { default as __fd_glob_2 } from "../content/docs/features/meta.json?collection=docs"
11
11
  import { default as __fd_glob_1 } from "../content/docs/changelog/meta.json?collection=docs"
12
12
  import { default as __fd_glob_0 } from "../content/docs/meta.json?collection=docs"
13
13
  import { server } from 'fumadocs-mdx/runtime/server';
@@ -18,4 +18,4 @@ const create = server<typeof Config, import("fumadocs-mdx/runtime/types").Intern
18
18
  }
19
19
  }>({"doc":{"passthroughs":["extractedReferences"]}});
20
20
 
21
- export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "changelog/meta.json": __fd_glob_1, "decisions/meta.json": __fd_glob_2, "features/meta.json": __fd_glob_3, "roadmap/meta.json": __fd_glob_4, }, {"index.mdx": __fd_glob_5, "changelog/index.mdx": __fd_glob_6, "decisions/turbo-package-manager.mdx": __fd_glob_7, "features/auth.mdx": __fd_glob_8, "features/hello.mdx": __fd_glob_9, "roadmap/index.mdx": __fd_glob_10, });
21
+ export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "changelog/meta.json": __fd_glob_1, "features/meta.json": __fd_glob_2, "decisions/meta.json": __fd_glob_3, "roadmap/meta.json": __fd_glob_4, }, {"index.mdx": __fd_glob_5, "changelog/index.mdx": __fd_glob_6, "decisions/turbo-package-manager.mdx": __fd_glob_7, "features/auth.mdx": __fd_glob_8, "features/hello.mdx": __fd_glob_9, "roadmap/index.mdx": __fd_glob_10, });
@@ -33,6 +33,56 @@
33
33
  "termsText": "By clicking continue, you agree to our",
34
34
  "termsLink": "Terms of Service",
35
35
  "andText": "and",
36
- "privacyLink": "Privacy Policy"
36
+ "privacyLink": "Privacy Policy",
37
+ "forgotPasswordLink": "Forgot password?",
38
+ "forgotPasswordTitle": "Reset your password",
39
+ "forgotPasswordSubtitle": "Enter your email and we'll send you a reset link.",
40
+ "forgotPasswordSent": "A reset link has been sent to {email}. Please check your inbox.",
41
+ "sendResetLink": "Send reset link",
42
+ "backToLogin": "Back to login",
43
+ "resetPasswordTitle": "Set new password",
44
+ "resetPasswordSubtitle": "Enter your new password below.",
45
+ "newPasswordLabel": "New password",
46
+ "resetPasswordButton": "Reset password",
47
+ "resetLinkExpired": "The reset link has expired. Please request a new one.",
48
+ "resetPasswordSuccess": "Password updated. Please log in."
49
+ },
50
+ "settings": {
51
+ "profile": {
52
+ "title": "Profile Settings",
53
+ "subtitle": "Manage your account settings and preferences.",
54
+ "profileCardTitle": "Profile",
55
+ "profileCardDescription": "Your public profile information.",
56
+ "avatarUploadComingSoon": "Avatar upload feature coming soon.",
57
+ "displayNameCardTitle": "Display Name",
58
+ "displayNameCardDescription": "Update your display name shown across the app.",
59
+ "displayNameLabel": "Display Name",
60
+ "displayNamePlaceholder": "Enter your display name",
61
+ "saveChanges": "Save Changes",
62
+ "displayNameUpdated": "Display name updated successfully",
63
+ "emailCardTitle": "Email Address",
64
+ "emailCardDescription": "Your primary email address.",
65
+ "contactSupportToChange": "To change your email, please contact support.",
66
+ "passwordCardTitle": "Password",
67
+ "passwordCardDescription": "Change your password.",
68
+ "changePassword": "Change Password"
69
+ }
70
+ },
71
+ "pages": {
72
+ "help": {
73
+ "title": "Help Center",
74
+ "description": "Find answers to common questions.",
75
+ "placeholder": "We are currently compiling our help documentation. Please check back later."
76
+ },
77
+ "privacy": {
78
+ "title": "Privacy Policy",
79
+ "description": "How we handle your data.",
80
+ "placeholder": "This template page is a placeholder. Replace it with your actual privacy policy before shipping."
81
+ },
82
+ "about": {
83
+ "title": "About Us",
84
+ "description": "Learn more about AgentDock.",
85
+ "placeholder": "This page is a placeholder. Replace it with your actual about information before shipping."
86
+ }
37
87
  }
38
88
  }
@@ -33,6 +33,56 @@
33
33
  "termsText": "点击继续即表示您同意我们的",
34
34
  "termsLink": "服务条款",
35
35
  "andText": "和",
36
- "privacyLink": "隐私政策"
36
+ "privacyLink": "隐私政策",
37
+ "forgotPasswordLink": "忘记密码?",
38
+ "forgotPasswordTitle": "重置密码",
39
+ "forgotPasswordSubtitle": "输入您的邮箱,我们将发送重置链接。",
40
+ "forgotPasswordSent": "重置链接已发送至 {email},请查收邮件。",
41
+ "sendResetLink": "发送重置链接",
42
+ "backToLogin": "返回登录",
43
+ "resetPasswordTitle": "设置新密码",
44
+ "resetPasswordSubtitle": "在下方输入您的新密码。",
45
+ "newPasswordLabel": "新密码",
46
+ "resetPasswordButton": "重置密码",
47
+ "resetLinkExpired": "重置链接已过期,请重新申请。",
48
+ "resetPasswordSuccess": "密码已更新,请登录。"
49
+ },
50
+ "settings": {
51
+ "profile": {
52
+ "title": "个人资料设置",
53
+ "subtitle": "管理您的账号设置和偏好。",
54
+ "profileCardTitle": "个人资料",
55
+ "profileCardDescription": "您的公开资料信息。",
56
+ "avatarUploadComingSoon": "头像上传功能即将推出。",
57
+ "displayNameCardTitle": "显示名",
58
+ "displayNameCardDescription": "更新您在应用中显示的昵称。",
59
+ "displayNameLabel": "显示名",
60
+ "displayNamePlaceholder": "输入您的显示名",
61
+ "saveChanges": "保存更改",
62
+ "displayNameUpdated": "显示名已更新",
63
+ "emailCardTitle": "邮箱地址",
64
+ "emailCardDescription": "您的主要邮箱地址。",
65
+ "contactSupportToChange": "如需修改邮箱,请联系支持。",
66
+ "passwordCardTitle": "密码",
67
+ "passwordCardDescription": "修改您的密码。",
68
+ "changePassword": "修改密码"
69
+ }
70
+ },
71
+ "pages": {
72
+ "help": {
73
+ "title": "帮助中心",
74
+ "description": "查找常见问题的答案。",
75
+ "placeholder": "我们正在整理帮助文档,请稍后查看。"
76
+ },
77
+ "privacy": {
78
+ "title": "隐私政策",
79
+ "description": "我们如何处理您的数据。",
80
+ "placeholder": "此页面为占位符。在发布前请替换为实际的隐私政策。"
81
+ },
82
+ "about": {
83
+ "title": "关于我们",
84
+ "description": "了解更多关于 AgentDock 的信息。",
85
+ "placeholder": "此页面为占位符。在发布前请替换为实际的关于我们信息。"
86
+ }
37
87
  }
38
88
  }
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/types/routes.d.ts";
3
+ import "./.next/dev/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,130 @@
1
+ 'use client'
2
+
3
+ import { useActionState, useEffect, useState } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { useTranslations } from 'next-intl'
7
+ import { useForm } from 'react-hook-form'
8
+ import { zodResolver } from '@hookform/resolvers/zod'
9
+ import { GalleryVerticalEnd } from 'lucide-react'
10
+ import { toast } from 'sonner'
11
+ import { Button } from '@/components/ui/button'
12
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
13
+ import { Input } from '@/components/ui/input'
14
+ import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
15
+ import { requestPasswordReset } from '@/features/auth'
16
+ import { forgotPasswordSchema, type ForgotPasswordInput } from '@/lib/validations/auth'
17
+ import type { ActionResult } from '@/core/types/auth'
18
+
19
+ export default function ForgotPasswordPage() {
20
+ const t = useTranslations('auth')
21
+ const routeParams = useParams<{ locale: string }>()
22
+ const locale = routeParams.locale ?? 'en'
23
+ const [isSent, setIsSent] = useState(false)
24
+ const [submittedEmail, setSubmittedEmail] = useState('')
25
+
26
+ const { register, handleSubmit, formState: { errors }, reset } = useForm<ForgotPasswordInput>({
27
+ resolver: zodResolver(forgotPasswordSchema),
28
+ defaultValues: { email: '' },
29
+ })
30
+
31
+ const [state, formAction, isPending] = useActionState<ActionResult | null, FormData>(
32
+ requestPasswordReset,
33
+ null,
34
+ )
35
+
36
+ useEffect(() => {
37
+ if (state?.error) toast.error(state.error)
38
+ }, [state])
39
+
40
+ async function onSubmit(data: ForgotPasswordInput) {
41
+ const formData = new FormData()
42
+ formData.append('email', data.email)
43
+ formData.append('locale', locale)
44
+ await formAction(formData)
45
+ setSubmittedEmail(data.email)
46
+ setIsSent(true)
47
+ reset()
48
+ }
49
+
50
+ if (isSent) {
51
+ return (
52
+ <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
53
+ <div className="flex w-full max-w-sm flex-col gap-6">
54
+ <Link href={`/${locale}`} className="flex items-center gap-2 self-center font-medium">
55
+ <div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
56
+ <GalleryVerticalEnd className="size-4" />
57
+ </div>
58
+ AgentDock
59
+ </Link>
60
+
61
+ <Card>
62
+ <CardHeader className="text-center">
63
+ <CardTitle className="text-xl">{t('forgotPasswordTitle')}</CardTitle>
64
+ </CardHeader>
65
+ <CardContent className="space-y-4">
66
+ <p className="text-center text-sm text-muted-foreground">
67
+ {t('forgotPasswordSent', { email: submittedEmail })}
68
+ </p>
69
+ <Button asChild variant="outline" className="w-full">
70
+ <Link href={`/${locale}/login`}>{t('backToLogin')}</Link>
71
+ </Button>
72
+ </CardContent>
73
+ </Card>
74
+ </div>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ return (
80
+ <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
81
+ <div className="flex w-full max-w-sm flex-col gap-6">
82
+ <Link href={`/${locale}`} className="flex items-center gap-2 self-center font-medium">
83
+ <div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
84
+ <GalleryVerticalEnd className="size-4" />
85
+ </div>
86
+ AgentDock
87
+ </Link>
88
+
89
+ <Card>
90
+ <CardHeader className="text-center">
91
+ <CardTitle className="text-xl">{t('forgotPasswordTitle')}</CardTitle>
92
+ <CardDescription>{t('forgotPasswordSubtitle')}</CardDescription>
93
+ </CardHeader>
94
+ <CardContent>
95
+ <form onSubmit={handleSubmit(onSubmit)}>
96
+ <input type="hidden" name="locale" value={locale} />
97
+ <FieldGroup>
98
+ <Field>
99
+ <FieldLabel htmlFor="email">{t('emailLabel')}</FieldLabel>
100
+ <Input
101
+ id="email"
102
+ type="email"
103
+ placeholder={t('emailPlaceholder')}
104
+ autoComplete="email"
105
+ {...register('email')}
106
+ />
107
+ {errors.email && (
108
+ <FieldDescription className="text-destructive">
109
+ {errors.email.message}
110
+ </FieldDescription>
111
+ )}
112
+ </Field>
113
+ <Field>
114
+ <Button type="submit" className="w-full" disabled={isPending}>
115
+ {isPending ? '\u2026' : t('sendResetLink')}
116
+ </Button>
117
+ <FieldDescription className="text-center">
118
+ <Link href={`/${locale}/login`} className="underline underline-offset-4">
119
+ {t('backToLogin')}
120
+ </Link>
121
+ </FieldDescription>
122
+ </Field>
123
+ </FieldGroup>
124
+ </form>
125
+ </CardContent>
126
+ </Card>
127
+ </div>
128
+ </div>
129
+ )
130
+ }
@@ -98,8 +98,14 @@ export default function LoginPage() {
98
98
  )}
99
99
  </Field>
100
100
  <Field>
101
- <div className="flex items-center">
101
+ <div className="flex items-center justify-between">
102
102
  <FieldLabel htmlFor="password">{t('passwordLabel')}</FieldLabel>
103
+ <Link
104
+ href={`/${locale}/forgot-password`}
105
+ className="text-sm text-muted-foreground underline underline-offset-4 hover:text-foreground"
106
+ >
107
+ {t('forgotPasswordLink')}
108
+ </Link>
103
109
  </div>
104
110
  <Input
105
111
  id="password"
@@ -0,0 +1,112 @@
1
+ 'use client'
2
+
3
+ import { useActionState, useEffect } from 'react'
4
+ import { useParams, useSearchParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { useTranslations } from 'next-intl'
7
+ import { useForm } from 'react-hook-form'
8
+ import { zodResolver } from '@hookform/resolvers/zod'
9
+ import { GalleryVerticalEnd } from 'lucide-react'
10
+ import { toast } from 'sonner'
11
+ import { Button } from '@/components/ui/button'
12
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
13
+ import { Input } from '@/components/ui/input'
14
+ import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
15
+ import { resetPassword } from '@/features/auth'
16
+ import { resetPasswordSchema, type ResetPasswordInput } from '@/lib/validations/auth'
17
+ import type { ActionResult } from '@/core/types/auth'
18
+
19
+ export default function ResetPasswordPage() {
20
+ const t = useTranslations('auth')
21
+ const routeParams = useParams<{ locale: string }>()
22
+ const searchParams = useSearchParams()
23
+ const locale = routeParams.locale ?? 'en'
24
+ const error = searchParams.get('error')
25
+
26
+ const { register, formState: { errors } } = useForm<ResetPasswordInput>({
27
+ resolver: zodResolver(resetPasswordSchema),
28
+ defaultValues: { password: '', confirmPassword: '' },
29
+ })
30
+
31
+ const [state, formAction, isPending] = useActionState<ActionResult | null, FormData>(
32
+ resetPassword,
33
+ null,
34
+ )
35
+
36
+ useEffect(() => {
37
+ if (state?.error) toast.error(state.error)
38
+ }, [state])
39
+
40
+ useEffect(() => {
41
+ if (error === 'link_expired') {
42
+ toast.error(t('resetLinkExpired'))
43
+ }
44
+ }, [error, t])
45
+
46
+ return (
47
+ <div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
48
+ <div className="flex w-full max-w-sm flex-col gap-6">
49
+ <Link href={`/${locale}`} className="flex items-center gap-2 self-center font-medium">
50
+ <div className="flex size-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
51
+ <GalleryVerticalEnd className="size-4" />
52
+ </div>
53
+ AgentDock
54
+ </Link>
55
+
56
+ <Card>
57
+ <CardHeader className="text-center">
58
+ <CardTitle className="text-xl">{t('resetPasswordTitle')}</CardTitle>
59
+ <CardDescription>{t('resetPasswordSubtitle')}</CardDescription>
60
+ </CardHeader>
61
+ <CardContent>
62
+ <form action={formAction}>
63
+ <input type="hidden" name="locale" value={locale} />
64
+ <FieldGroup>
65
+ <Field>
66
+ <FieldLabel htmlFor="password">{t('newPasswordLabel')}</FieldLabel>
67
+ <Input
68
+ id="password"
69
+ type="password"
70
+ placeholder={t('passwordPlaceholder')}
71
+ autoComplete="new-password"
72
+ {...register('password')}
73
+ />
74
+ {errors.password && (
75
+ <FieldDescription className="text-destructive">
76
+ {errors.password.message}
77
+ </FieldDescription>
78
+ )}
79
+ </Field>
80
+ <Field>
81
+ <FieldLabel htmlFor="confirmPassword">{t('confirmPasswordLabel')}</FieldLabel>
82
+ <Input
83
+ id="confirmPassword"
84
+ type="password"
85
+ placeholder={t('confirmPasswordPlaceholder')}
86
+ autoComplete="new-password"
87
+ {...register('confirmPassword')}
88
+ />
89
+ {errors.confirmPassword && (
90
+ <FieldDescription className="text-destructive">
91
+ {errors.confirmPassword.message}
92
+ </FieldDescription>
93
+ )}
94
+ </Field>
95
+ <Field>
96
+ <Button type="submit" className="w-full" disabled={isPending}>
97
+ {isPending ? '\u2026' : t('resetPasswordButton')}
98
+ </Button>
99
+ <FieldDescription className="text-center">
100
+ <Link href={`/${locale}/login`} className="underline underline-offset-4">
101
+ {t('backToLogin')}
102
+ </Link>
103
+ </FieldDescription>
104
+ </Field>
105
+ </FieldGroup>
106
+ </form>
107
+ </CardContent>
108
+ </Card>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
@@ -0,0 +1,9 @@
1
+ import { ReactNode } from 'react'
2
+
3
+ export default function SettingsLayout({ children }: { children: ReactNode }) {
4
+ return (
5
+ <div className="container mx-auto max-w-4xl px-4 py-10">
6
+ {children}
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,9 @@
1
+ import { redirect } from 'next/navigation'
2
+ import { defaultLocale } from '@/i18n/config'
3
+
4
+ export default function SettingsPage({ params }: { params: Promise<{ locale: string }> }) {
5
+ return (async () => {
6
+ const { locale } = await params
7
+ redirect(`/${locale}/settings/profile`)
8
+ })()
9
+ }
@@ -0,0 +1,97 @@
1
+ import { redirect } from 'next/navigation'
2
+ import Link from 'next/link'
3
+ import { getCurrentUser } from '@/features/auth/server'
4
+ import { ProfileForm } from '@/components/profile/profile-form'
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Separator } from '@/components/ui/separator'
9
+
10
+ export default async function ProfilePage({
11
+ params,
12
+ }: {
13
+ params: Promise<{ locale: string }>
14
+ }) {
15
+ const { locale } = await params
16
+ const user = await getCurrentUser()
17
+
18
+ if (!user) {
19
+ redirect(`/${locale}/login`)
20
+ }
21
+
22
+ const displayName = user.user_metadata?.display_name ?? ''
23
+ const email = user.email ?? ''
24
+ const avatarUrl = user.user_metadata?.avatar_url ?? ''
25
+ const initials = email.charAt(0).toUpperCase()
26
+
27
+ return (
28
+ <div className="space-y-6">
29
+ <div>
30
+ <h1 className="text-2xl font-semibold">Profile Settings</h1>
31
+ <p className="text-muted-foreground">Manage your account settings and preferences.</p>
32
+ </div>
33
+ <Separator />
34
+
35
+ {/* Avatar & Basic Info */}
36
+ <Card>
37
+ <CardHeader>
38
+ <CardTitle>Profile</CardTitle>
39
+ <CardDescription>Your public profile information.</CardDescription>
40
+ </CardHeader>
41
+ <CardContent className="space-y-4">
42
+ <div className="flex items-center gap-4">
43
+ <Avatar className="h-16 w-16">
44
+ <AvatarImage src={avatarUrl} alt={displayName || email} />
45
+ <AvatarFallback className="text-lg">{initials}</AvatarFallback>
46
+ </Avatar>
47
+ <div>
48
+ <p className="font-medium">{displayName || 'No display name set'}</p>
49
+ <p className="text-sm text-muted-foreground">{email}</p>
50
+ </div>
51
+ </div>
52
+ <p className="text-sm text-muted-foreground">
53
+ Avatar upload feature coming soon.
54
+ </p>
55
+ </CardContent>
56
+ </Card>
57
+
58
+ {/* Display Name */}
59
+ <Card>
60
+ <CardHeader>
61
+ <CardTitle>Display Name</CardTitle>
62
+ <CardDescription>Update your display name shown across the app.</CardDescription>
63
+ </CardHeader>
64
+ <CardContent>
65
+ <ProfileForm defaultName={displayName} locale={locale} />
66
+ </CardContent>
67
+ </Card>
68
+
69
+ {/* Email */}
70
+ <Card>
71
+ <CardHeader>
72
+ <CardTitle>Email Address</CardTitle>
73
+ <CardDescription>Your primary email address.</CardDescription>
74
+ </CardHeader>
75
+ <CardContent>
76
+ <p className="text-sm">{email}</p>
77
+ <p className="mt-2 text-sm text-muted-foreground">
78
+ To change your email, please contact support.
79
+ </p>
80
+ </CardContent>
81
+ </Card>
82
+
83
+ {/* Password */}
84
+ <Card>
85
+ <CardHeader>
86
+ <CardTitle>Password</CardTitle>
87
+ <CardDescription>Change your password.</CardDescription>
88
+ </CardHeader>
89
+ <CardContent>
90
+ <Button asChild variant="outline">
91
+ <Link href={`/${locale}/forgot-password`}>Change Password</Link>
92
+ </Button>
93
+ </CardContent>
94
+ </Card>
95
+ </div>
96
+ )
97
+ }
@@ -0,0 +1,25 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
2
+
3
+ export default async function AboutPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ locale: string }>
7
+ }) {
8
+ const { locale } = await params
9
+
10
+ return (
11
+ <main className="mx-auto max-w-3xl px-4 py-10">
12
+ <Card>
13
+ <CardHeader>
14
+ <CardTitle>About Us</CardTitle>
15
+ <CardDescription>Learn more about AgentDock.</CardDescription>
16
+ </CardHeader>
17
+ <CardContent>
18
+ <p className="text-muted-foreground">
19
+ This page is a placeholder. Replace it with your actual about information before shipping.
20
+ </p>
21
+ </CardContent>
22
+ </Card>
23
+ </main>
24
+ )
25
+ }
@@ -0,0 +1,25 @@
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
2
+
3
+ export default async function HelpPage({
4
+ params,
5
+ }: {
6
+ params: Promise<{ locale: string }>
7
+ }) {
8
+ const { locale } = await params
9
+
10
+ return (
11
+ <main className="mx-auto max-w-3xl px-4 py-10">
12
+ <Card>
13
+ <CardHeader>
14
+ <CardTitle>Help Center</CardTitle>
15
+ <CardDescription>Find answers to common questions.</CardDescription>
16
+ </CardHeader>
17
+ <CardContent>
18
+ <p className="text-muted-foreground">
19
+ We are currently compiling our help documentation. Please check back later.
20
+ </p>
21
+ </CardContent>
22
+ </Card>
23
+ </main>
24
+ )
25
+ }
@@ -1,4 +1,4 @@
1
- import Link from 'next/link'
1
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
2
2
 
3
3
  export default async function PrivacyPage({
4
4
  params,
@@ -9,15 +9,17 @@ export default async function PrivacyPage({
9
9
 
10
10
  return (
11
11
  <main className="mx-auto max-w-3xl px-4 py-10">
12
- <h1 className="text-2xl font-semibold">Privacy Policy</h1>
13
- <p className="mt-4 text-muted-foreground">
14
- This template page is a placeholder. Replace it with your actual privacy policy before shipping.
15
- </p>
16
- <p className="mt-6">
17
- <Link href={`/${locale}/signup`} className="underline underline-offset-4">
18
- Back to sign up
19
- </Link>
20
- </p>
12
+ <Card>
13
+ <CardHeader>
14
+ <CardTitle>Privacy Policy</CardTitle>
15
+ <CardDescription>How we handle your data.</CardDescription>
16
+ </CardHeader>
17
+ <CardContent>
18
+ <p className="text-muted-foreground">
19
+ This template page is a placeholder. Replace it with your actual privacy policy before shipping.
20
+ </p>
21
+ </CardContent>
22
+ </Card>
21
23
  </main>
22
24
  )
23
25
  }
@@ -2,18 +2,24 @@ import { NextRequest, NextResponse } from 'next/server'
2
2
  import { getServerClient } from '@/infra/db/client'
3
3
  import { defaultLocale } from '@/i18n/config'
4
4
 
5
+ const ALLOWED_NEXT = ['/reset-password']
6
+
5
7
  export async function GET(request: NextRequest) {
6
8
  const { searchParams, origin } = new URL(request.url)
7
9
  const code = searchParams.get('code')
8
10
  const nextParam = searchParams.get('next')
9
- const next = nextParam && nextParam.startsWith('/') ? nextParam : `/${defaultLocale}/dashboard`
11
+
12
+ // Whitelist check to prevent open redirect attacks
13
+ const safePath = nextParam && ALLOWED_NEXT.includes(nextParam)
14
+ ? nextParam
15
+ : `/${defaultLocale}/dashboard`
10
16
 
11
17
  if (code) {
12
18
  const supabase = await getServerClient()
13
19
  const { error } = await supabase.auth.exchangeCodeForSession(code)
14
20
 
15
21
  if (!error) {
16
- return NextResponse.redirect(`${origin}${next}`)
22
+ return NextResponse.redirect(`${origin}${safePath}`)
17
23
  }
18
24
  }
19
25
 
@@ -2,24 +2,14 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import {
5
- IconCamera,
6
- IconChartBar,
7
5
  IconDashboard,
8
- IconDatabase,
9
- IconFileAi,
10
- IconFileDescription,
11
- IconFileWord,
12
- IconFolder,
13
6
  IconHelp,
7
+ IconInfoCircle,
14
8
  IconInnerShadowTop,
15
- IconListDetails,
16
- IconReport,
17
- IconSearch,
18
9
  IconSettings,
19
- IconUsers,
10
+ IconShield,
20
11
  } from "@tabler/icons-react"
21
12
 
22
- import { NavDocuments } from "@/components/dashboard/nav-documents"
23
13
  import { NavMain } from "@/components/dashboard/nav-main"
24
14
  import { NavSecondary } from "@/components/dashboard/nav-secondary"
25
15
  import { NavUser } from "@/components/dashboard/nav-user"
@@ -33,131 +23,45 @@ import {
33
23
  SidebarMenuItem,
34
24
  } from "@/components/ui/sidebar"
35
25
 
36
- const data = {
37
- user: {
38
- name: "shadcn",
39
- email: "m@example.com",
40
- avatar: "/avatars/shadcn.jpg",
41
- },
42
- navMain: [
26
+ export function AppSidebar({
27
+ user,
28
+ locale,
29
+ ...props
30
+ }: React.ComponentProps<typeof Sidebar> & {
31
+ user: { name: string; email: string; avatar: string }
32
+ locale: string
33
+ }) {
34
+ const navMain = [
43
35
  {
44
36
  title: "Dashboard",
45
- url: "#",
37
+ url: `/${locale}/dashboard`,
46
38
  icon: IconDashboard,
47
39
  },
48
- {
49
- title: "Lifecycle",
50
- url: "#",
51
- icon: IconListDetails,
52
- },
53
- {
54
- title: "Analytics",
55
- url: "#",
56
- icon: IconChartBar,
57
- },
58
- {
59
- title: "Projects",
60
- url: "#",
61
- icon: IconFolder,
62
- },
63
- {
64
- title: "Team",
65
- url: "#",
66
- icon: IconUsers,
67
- },
68
- ],
69
- navClouds: [
70
- {
71
- title: "Capture",
72
- icon: IconCamera,
73
- isActive: true,
74
- url: "#",
75
- items: [
76
- {
77
- title: "Active Proposals",
78
- url: "#",
79
- },
80
- {
81
- title: "Archived",
82
- url: "#",
83
- },
84
- ],
85
- },
86
- {
87
- title: "Proposal",
88
- icon: IconFileDescription,
89
- url: "#",
90
- items: [
91
- {
92
- title: "Active Proposals",
93
- url: "#",
94
- },
95
- {
96
- title: "Archived",
97
- url: "#",
98
- },
99
- ],
100
- },
101
- {
102
- title: "Prompts",
103
- icon: IconFileAi,
104
- url: "#",
105
- items: [
106
- {
107
- title: "Active Proposals",
108
- url: "#",
109
- },
110
- {
111
- title: "Archived",
112
- url: "#",
113
- },
114
- ],
115
- },
116
- ],
117
- navSecondary: [
118
40
  {
119
41
  title: "Settings",
120
- url: "#",
42
+ url: `/${locale}/settings/profile`,
121
43
  icon: IconSettings,
122
44
  },
45
+ ]
46
+
47
+ const navSecondary = [
123
48
  {
124
- title: "Get Help",
125
- url: "#",
49
+ title: "Help",
50
+ url: `/${locale}/help`,
126
51
  icon: IconHelp,
127
52
  },
128
53
  {
129
- title: "Search",
130
- url: "#",
131
- icon: IconSearch,
132
- },
133
- ],
134
- documents: [
135
- {
136
- name: "Data Library",
137
- url: "#",
138
- icon: IconDatabase,
139
- },
140
- {
141
- name: "Reports",
142
- url: "#",
143
- icon: IconReport,
54
+ title: "Privacy Policy",
55
+ url: `/${locale}/privacy`,
56
+ icon: IconShield,
144
57
  },
145
58
  {
146
- name: "Word Assistant",
147
- url: "#",
148
- icon: IconFileWord,
59
+ title: "About",
60
+ url: `/${locale}/about`,
61
+ icon: IconInfoCircle,
149
62
  },
150
- ],
151
- }
63
+ ]
152
64
 
153
- export function AppSidebar({
154
- user,
155
- locale,
156
- ...props
157
- }: React.ComponentProps<typeof Sidebar> & {
158
- user: { name: string; email: string; avatar: string }
159
- locale: string
160
- }) {
161
65
  return (
162
66
  <Sidebar collapsible="offcanvas" {...props}>
163
67
  <SidebarHeader>
@@ -176,9 +80,8 @@ export function AppSidebar({
176
80
  </SidebarMenu>
177
81
  </SidebarHeader>
178
82
  <SidebarContent>
179
- <NavMain items={data.navMain} />
180
- <NavDocuments items={data.documents} />
181
- <NavSecondary items={data.navSecondary} className="mt-auto" />
83
+ <NavMain items={navMain} />
84
+ <NavSecondary items={navSecondary} className="mt-auto" />
182
85
  </SidebarContent>
183
86
  <SidebarFooter>
184
87
  <NavUser user={user} locale={locale} />
@@ -1,10 +1,9 @@
1
1
  "use client"
2
2
 
3
+ import Link from 'next/link'
3
4
  import {
4
- IconCreditCard,
5
5
  IconDotsVertical,
6
6
  IconLogout,
7
- IconNotification,
8
7
  IconUserCircle,
9
8
  } from "@tabler/icons-react"
10
9
 
@@ -87,17 +86,11 @@ export function NavUser({
87
86
  </DropdownMenuLabel>
88
87
  <DropdownMenuSeparator />
89
88
  <DropdownMenuGroup>
90
- <DropdownMenuItem>
91
- <IconUserCircle />
92
- Account
93
- </DropdownMenuItem>
94
- <DropdownMenuItem>
95
- <IconCreditCard />
96
- Billing
97
- </DropdownMenuItem>
98
- <DropdownMenuItem>
99
- <IconNotification />
100
- Notifications
89
+ <DropdownMenuItem asChild>
90
+ <Link href={`/${locale}/settings/profile`} className="flex w-full items-center gap-2">
91
+ <IconUserCircle />
92
+ Account
93
+ </Link>
101
94
  </DropdownMenuItem>
102
95
  </DropdownMenuGroup>
103
96
  <DropdownMenuSeparator />
@@ -0,0 +1,64 @@
1
+ 'use client'
2
+
3
+ import { useActionState, useEffect } from 'react'
4
+ import { useForm } from 'react-hook-form'
5
+ import { zodResolver } from '@hookform/resolvers/zod'
6
+ import { toast } from 'sonner'
7
+ import { Button } from '@/components/ui/button'
8
+ import { Input } from '@/components/ui/input'
9
+ import { Field, FieldDescription, FieldGroup, FieldLabel } from '@/components/ui/field'
10
+ import { updateDisplayName } from '@/features/auth'
11
+ import { displayNameSchema, type DisplayNameInput } from '@/lib/validations/auth'
12
+ import type { ActionResult } from '@/core/types/auth'
13
+
14
+ interface ProfileFormProps {
15
+ defaultName: string
16
+ locale: string
17
+ }
18
+
19
+ export function ProfileForm({ defaultName, locale }: ProfileFormProps) {
20
+ const { register, formState: { errors }, reset } = useForm<DisplayNameInput>({
21
+ resolver: zodResolver(displayNameSchema),
22
+ defaultValues: { name: defaultName },
23
+ })
24
+
25
+ const [state, formAction, isPending] = useActionState<ActionResult | null, FormData>(
26
+ updateDisplayName,
27
+ null,
28
+ )
29
+
30
+ useEffect(() => {
31
+ if (state?.error) {
32
+ toast.error(state.error)
33
+ } else if (state?.data !== undefined && !state.error) {
34
+ toast.success('Display name updated successfully')
35
+ }
36
+ }, [state])
37
+
38
+ return (
39
+ <form action={formAction}>
40
+ <input type="hidden" name="locale" value={locale} />
41
+ <FieldGroup>
42
+ <Field>
43
+ <FieldLabel htmlFor="name">Display Name</FieldLabel>
44
+ <Input
45
+ id="name"
46
+ type="text"
47
+ placeholder="Enter your display name"
48
+ {...register('name')}
49
+ />
50
+ {errors.name && (
51
+ <FieldDescription className="text-destructive">
52
+ {errors.name.message}
53
+ </FieldDescription>
54
+ )}
55
+ </Field>
56
+ <Field>
57
+ <Button type="submit" disabled={isPending}>
58
+ {isPending ? '\u2026' : 'Save Changes'}
59
+ </Button>
60
+ </Field>
61
+ </FieldGroup>
62
+ </form>
63
+ )
64
+ }
@@ -6,4 +6,7 @@ export interface IAuthRepository {
6
6
  signOut(): Promise<void>
7
7
  getSession(): Promise<AuthUser | null>
8
8
  signInWithOAuth(provider: 'github', nextPath?: string): Promise<{ url: string }>
9
+ updateDisplayName(name: string): Promise<AuthResult>
10
+ requestPasswordReset(email: string, redirectTo: string): Promise<void>
11
+ resetPassword(password: string): Promise<AuthResult>
9
12
  }
@@ -4,6 +4,9 @@ export type SignInArgs = { email: string; password: string }
4
4
  export type SignUpArgs = { email: string; password: string; confirmPassword: string }
5
5
  export type SignUpSuccessData = { success: true; email: string }
6
6
  export type OAuthData = { url: string }
7
+ export type RequestPasswordResetArgs = { email: string }
8
+ export type ResetPasswordArgs = { password: string; confirmPassword: string }
9
+ export type UpdateDisplayNameArgs = { name: string }
7
10
 
8
11
  export interface AuthFeatureContract {
9
12
  signIn(args: SignInArgs): Promise<ActionResult>
@@ -11,4 +14,7 @@ export interface AuthFeatureContract {
11
14
  signOut(): Promise<void>
12
15
  signInWithGithub(): Promise<ActionResult<OAuthData>>
13
16
  signInWithGithubForLocale(locale: string): Promise<ActionResult<OAuthData>>
17
+ requestPasswordReset(args: RequestPasswordResetArgs): Promise<ActionResult>
18
+ resetPassword(args: ResetPasswordArgs): Promise<ActionResult>
19
+ updateDisplayName(args: UpdateDisplayNameArgs): Promise<ActionResult>
14
20
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { redirect } from 'next/navigation'
4
4
  import { getAuthRepository } from '@/infra/providers'
5
- import { signInSchema, signUpSchema } from '@/lib/validations/auth'
5
+ import { signInSchema, signUpSchema, forgotPasswordSchema, resetPasswordSchema, displayNameSchema } from '@/lib/validations/auth'
6
6
  import type { ActionResult } from '@/core/types/auth'
7
7
  import { defaultLocale, isLocale } from '@/i18n/config'
8
8
  import type { SignUpSuccessData, OAuthData } from './__contract__'
@@ -84,3 +84,72 @@ export async function signInWithGithubForLocale(
84
84
  return { data: null, error: msg }
85
85
  }
86
86
  }
87
+
88
+ export async function requestPasswordReset(
89
+ _prevState: ActionResult | null,
90
+ formData: FormData,
91
+ ): Promise<ActionResult<void>> {
92
+ const parsed = forgotPasswordSchema.safeParse({
93
+ email: formData.get('email'),
94
+ })
95
+
96
+ if (!parsed.success) {
97
+ return { data: null, error: parsed.error.issues[0]?.message ?? '输入无效' }
98
+ }
99
+
100
+ const repo = getAuthRepository()
101
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'
102
+ const locale = normalizeLocale(formData.get('locale'))
103
+ const redirectTo = `${appUrl}/auth/callback?next=/${locale}/reset-password`
104
+
105
+ // Always return success to prevent email enumeration
106
+ await repo.requestPasswordReset(parsed.data.email, redirectTo)
107
+
108
+ return { data: undefined as void, error: null }
109
+ }
110
+
111
+ export async function resetPassword(
112
+ _prevState: ActionResult | null,
113
+ formData: FormData,
114
+ ): Promise<ActionResult<void>> {
115
+ const parsed = resetPasswordSchema.safeParse({
116
+ password: formData.get('password'),
117
+ confirmPassword: formData.get('confirmPassword'),
118
+ })
119
+
120
+ if (!parsed.success) {
121
+ return { data: null, error: parsed.error.issues[0]?.message ?? '输入无效' }
122
+ }
123
+
124
+ const repo = getAuthRepository()
125
+ const result = await repo.resetPassword(parsed.data.password)
126
+
127
+ if (result.error) {
128
+ return { data: null, error: result.error }
129
+ }
130
+
131
+ const locale = normalizeLocale(formData.get('locale'))
132
+ redirect(`/${locale}/login`)
133
+ }
134
+
135
+ export async function updateDisplayName(
136
+ _prevState: ActionResult | null,
137
+ formData: FormData,
138
+ ): Promise<ActionResult<void>> {
139
+ const parsed = displayNameSchema.safeParse({
140
+ name: formData.get('name'),
141
+ })
142
+
143
+ if (!parsed.success) {
144
+ return { data: null, error: parsed.error.issues[0]?.message ?? '输入无效' }
145
+ }
146
+
147
+ const repo = getAuthRepository()
148
+ const result = await repo.updateDisplayName(parsed.data.name)
149
+
150
+ if (result.error) {
151
+ return { data: null, error: result.error }
152
+ }
153
+
154
+ return { data: undefined as void, error: null }
155
+ }
@@ -1 +1,10 @@
1
- export { signIn, signUp, signOut, signInWithGithub, signInWithGithubForLocale } from './actions'
1
+ export {
2
+ signIn,
3
+ signUp,
4
+ signOut,
5
+ signInWithGithub,
6
+ signInWithGithubForLocale,
7
+ requestPasswordReset,
8
+ resetPassword,
9
+ updateDisplayName,
10
+ } from './actions'
@@ -1,9 +1,12 @@
1
1
  import { cache } from 'react'
2
+ // eslint-disable-next-line no-restricted-imports -- Infrastructure helper for reading current session; returns raw Supabase user with metadata
2
3
  import { getServerClient } from '@/infra/db/client'
3
4
 
4
5
  /**
5
6
  * Returns the current authenticated user for the active request.
6
7
  * Wrapped in React cache() so repeated reads in one request are deduplicated.
8
+ * Note: This is an infrastructure helper that returns the raw Supabase user
9
+ * object (including user_metadata) for use in Server Components.
7
10
  */
8
11
  export const getCurrentUser = cache(async () => {
9
12
  const supabase = await getServerClient()
@@ -60,4 +60,29 @@ export class SupabaseAuthRepository implements IAuthRepository {
60
60
  }
61
61
  return { url: data.url }
62
62
  }
63
+
64
+ async updateDisplayName(name: string): Promise<AuthResult> {
65
+ const supabase = await getServerClient()
66
+ const { error } = await supabase.auth.updateUser({
67
+ data: { display_name: name },
68
+ })
69
+ if (error) {
70
+ return { user: null, error: error.message }
71
+ }
72
+ return { user: null, error: null }
73
+ }
74
+
75
+ async requestPasswordReset(email: string, redirectTo: string): Promise<void> {
76
+ const supabase = await getServerClient()
77
+ await supabase.auth.resetPasswordForEmail(email, { redirectTo })
78
+ }
79
+
80
+ async resetPassword(password: string): Promise<AuthResult> {
81
+ const supabase = await getServerClient()
82
+ const { error } = await supabase.auth.updateUser({ password })
83
+ if (error) {
84
+ return { user: null, error: error.message }
85
+ }
86
+ return { user: null, error: null }
87
+ }
63
88
  }
@@ -16,5 +16,26 @@ export const signUpSchema = z
16
16
  path: ['confirmPassword'],
17
17
  })
18
18
 
19
+ export const forgotPasswordSchema = z.object({
20
+ email: z.string().email('请输入有效邮箱'),
21
+ })
22
+
23
+ export const resetPasswordSchema = z
24
+ .object({
25
+ password: z.string().min(8, '密码至少 8 位'),
26
+ confirmPassword: z.string(),
27
+ })
28
+ .refine((data) => data.password === data.confirmPassword, {
29
+ message: '两次密码不一致',
30
+ path: ['confirmPassword'],
31
+ })
32
+
33
+ export const displayNameSchema = z.object({
34
+ name: z.string().min(1, '显示名不能为空').max(50, '显示名最多 50 字符'),
35
+ })
36
+
19
37
  export type SignInInput = z.infer<typeof signInSchema>
20
38
  export type SignUpInput = z.infer<typeof signUpSchema>
39
+ export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>
40
+ export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>
41
+ export type DisplayNameInput = z.infer<typeof displayNameSchema>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cogito.ai/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "AgentDock CLI – scaffold projects for humans and AI agents",
6
6
  "publishConfig": {