create-mantiq 0.5.5 → 0.5.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mantiq",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Scaffold a new MantiqJS application",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
1
  import { Model } from '@mantiq/database'
2
2
  import type { Authenticatable } from '@mantiq/auth'
3
+ import { applyHasApiTokens } from '@mantiq/auth'
3
4
 
4
5
  export class User extends Model implements Authenticatable {
5
6
  static override table = 'users'
@@ -15,4 +16,13 @@ export class User extends Model implements Authenticatable {
15
16
  getRememberToken(): string | null { return (this.getAttribute('remember_token') as string) ?? null }
16
17
  setRememberToken(token: string | null): void { this.setAttribute('remember_token', token) }
17
18
  getRememberTokenName(): string { return 'remember_token' }
19
+
20
+ // Token methods: createToken(), tokens(), currentAccessToken(), tokenCan(), tokenCant()
21
+ declare createToken: (name: string, abilities?: string[], expiresAt?: Date) => Promise<{ accessToken: any; plainTextToken: string }>
22
+ declare tokens: () => any
23
+ declare currentAccessToken: () => any
24
+ declare tokenCan: (ability: string) => boolean
25
+ declare tokenCant: (ability: string) => boolean
18
26
  }
27
+
28
+ applyHasApiTokens(User)
@@ -10,5 +10,5 @@ export default {
10
10
  basePath: import.meta.dir + '/..',
11
11
 
12
12
  // Global middleware applied to every request
13
- middleware: ['cors', 'encrypt.cookies', 'session', 'heartbeat'],
13
+ middleware: ['cors', 'encrypt.cookies', 'session', 'csrf'],
14
14
  }
package/src/index.ts CHANGED
@@ -181,6 +181,24 @@ if (kit) {
181
181
  }
182
182
  }
183
183
  }
184
+ } else {
185
+ // API-only: overlay token-based auth stubs
186
+ const stubsDir = resolve(import.meta.dir, '..', 'stubs')
187
+ const apiOnlyFiles = [
188
+ { stub: 'api-only/routes/api.ts.stub', target: 'routes/api.ts' },
189
+ { stub: 'shared/app/Http/Controllers/ApiAuthController.ts.stub', target: 'app/Http/Controllers/ApiAuthController.ts' },
190
+ { stub: 'shared/database/seeders/DatabaseSeeder.ts.stub', target: 'database/seeders/DatabaseSeeder.ts' },
191
+ { stub: 'shared/database/factories/UserFactory.ts.stub', target: 'database/factories/UserFactory.ts' },
192
+ ]
193
+ for (const { stub, target } of apiOnlyFiles) {
194
+ const src = resolve(stubsDir, stub)
195
+ if (existsSync(src)) {
196
+ const dest = resolve(projectDir, target)
197
+ mkdirSync(dirname(dest), { recursive: true })
198
+ await Bun.write(dest, Bun.file(src))
199
+ fileCount++
200
+ }
201
+ }
184
202
  }
185
203
  console.log(` ${dim(`${fileCount} files created`)}`)
186
204
 
@@ -0,0 +1,119 @@
1
+ import type { Router } from '@mantiq/core'
2
+ import { MantiqResponse } from '@mantiq/core'
3
+ import { ApiAuthController } from '../app/Http/Controllers/ApiAuthController.ts'
4
+ import { User } from '../app/Models/User.ts'
5
+
6
+ export default function (router: Router) {
7
+ router.get('/api/ping', () => {
8
+ return MantiqResponse.json({ status: 'ok', timestamp: new Date().toISOString() })
9
+ })
10
+
11
+ // ── Token Auth ─────────────────────────────────────────────────────────
12
+ // Sanctum-style: register/login return a bearer token.
13
+ // Send it as: Authorization: Bearer {token}
14
+
15
+ router.post('/api/register', [ApiAuthController, 'register'])
16
+ router.post('/api/login', [ApiAuthController, 'login'])
17
+ router.post('/api/logout', [ApiAuthController, 'logout']).middleware('auth:api')
18
+ router.get('/api/user', [ApiAuthController, 'user']).middleware('auth:api')
19
+ router.get('/api/tokens', [ApiAuthController, 'tokens']).middleware('auth:api')
20
+
21
+ // ── Users CRUD (protected) ────────────────────────────────────────────
22
+
23
+ router.get('/api/users', async (request: any) => {
24
+ const search = request.query('search') ?? ''
25
+ const page = Math.max(1, Number(request.query('page') ?? 1))
26
+ const perPage = Math.min(100, Math.max(1, Number(request.query('per_page') ?? 10)))
27
+ const sortBy = request.query('sort') ?? 'created_at'
28
+ const sortDir = request.query('dir') === 'asc' ? 'asc' : 'desc'
29
+
30
+ let query = User.query()
31
+
32
+ if (search) {
33
+ query = query.where('name', 'LIKE', `%${search}%`)
34
+ .orWhere('email', 'LIKE', `%${search}%`) as any
35
+ }
36
+
37
+ const total = await (User.query() as any).count() as number
38
+ const filteredQuery = search
39
+ ? User.query().where('name', 'LIKE', `%${search}%`).orWhere('email', 'LIKE', `%${search}%`) as any
40
+ : User.query()
41
+
42
+ const filteredTotal = await filteredQuery.count() as number
43
+
44
+ const users = search
45
+ ? await User.query()
46
+ .where('name', 'LIKE', `%${search}%`)
47
+ .orWhere('email', 'LIKE', `%${search}%`)
48
+ .orderBy(sortBy, sortDir)
49
+ .limit(perPage)
50
+ .offset((page - 1) * perPage)
51
+ .get() as any[]
52
+ : await User.query()
53
+ .orderBy(sortBy, sortDir)
54
+ .limit(perPage)
55
+ .offset((page - 1) * perPage)
56
+ .get() as any[]
57
+
58
+ return MantiqResponse.json({
59
+ data: users.map((u: any) => u.toObject()),
60
+ meta: {
61
+ total,
62
+ filtered_total: filteredTotal,
63
+ page,
64
+ per_page: perPage,
65
+ last_page: Math.ceil(filteredTotal / perPage),
66
+ },
67
+ })
68
+ }).middleware('auth:api')
69
+
70
+ router.post('/api/users', async (request: any) => {
71
+ const body = await request.input()
72
+ if (!body?.name || !body?.email || !body?.password) {
73
+ return MantiqResponse.json({ error: 'Name, email and password are required.' }, 422)
74
+ }
75
+
76
+ const existing = await User.where('email', body.email).first()
77
+ if (existing) {
78
+ return MantiqResponse.json({ error: 'Email already exists.' }, 422)
79
+ }
80
+
81
+ const { HashManager } = await import('@mantiq/core')
82
+ const hasher = new HashManager({ bcrypt: { rounds: 10 } })
83
+ const user = await User.create({
84
+ name: body.name,
85
+ email: body.email,
86
+ password: await hasher.make(body.password),
87
+ })
88
+
89
+ return MantiqResponse.json({ data: user.toObject() }, 201)
90
+ }).middleware('auth:api')
91
+
92
+ router.put('/api/users/:id', async (request: any) => {
93
+ const id = request.param('id')
94
+ const body = await request.input()
95
+ const user = await User.find(Number(id))
96
+ if (!user) return MantiqResponse.json({ error: 'User not found.' }, 404)
97
+
98
+ if (body.name) user.setAttribute('name', body.name)
99
+ if (body.email) user.setAttribute('email', body.email)
100
+
101
+ if (body.password) {
102
+ const { HashManager } = await import('@mantiq/core')
103
+ const hasher = new HashManager({ bcrypt: { rounds: 10 } })
104
+ user.setAttribute('password', await hasher.make(body.password))
105
+ }
106
+
107
+ await user.save()
108
+ return MantiqResponse.json({ data: user.toObject() })
109
+ }).middleware('auth:api')
110
+
111
+ router.delete('/api/users/:id', async (request: any) => {
112
+ const id = request.param('id')
113
+ const user = await User.find(Number(id))
114
+ if (!user) return MantiqResponse.json({ error: 'User not found.' }, 404)
115
+
116
+ await user.delete()
117
+ return MantiqResponse.json({ success: true })
118
+ }).middleware('auth:api')
119
+ }
@@ -11,8 +11,8 @@ interface LoginProps {
11
11
  }
12
12
 
13
13
  export default function Login({ appName = 'Mantiq', navigate }: LoginProps) {
14
- const [email, setEmail] = useState('admin@example.com')
15
- const [password, setPassword] = useState('password')
14
+ const [email, setEmail] = useState('')
15
+ const [password, setPassword] = useState('')
16
16
  const [error, setError] = useState('')
17
17
  const [loading, setLoading] = useState(false)
18
18
 
@@ -81,7 +81,7 @@ export default function Login({ appName = 'Mantiq', navigate }: LoginProps) {
81
81
  value={email}
82
82
  onChange={(e) => setEmail(e.target.value)}
83
83
  required
84
- placeholder="admin@example.com"
84
+ placeholder="you@example.com"
85
85
  autoComplete="email"
86
86
  />
87
87
  </div>
@@ -0,0 +1,119 @@
1
+ import type { MantiqRequest } from '@mantiq/core'
2
+ import { MantiqResponse, HashManager } from '@mantiq/core'
3
+ import { auth } from '@mantiq/auth'
4
+ import { User } from '../../Models/User.ts'
5
+
6
+ /**
7
+ * Sanctum-style token authentication for API-only apps.
8
+ * Issues bearer tokens instead of session cookies.
9
+ */
10
+ export class ApiAuthController {
11
+ async register(request: MantiqRequest): Promise<Response> {
12
+ const body = await request.input() as { name?: string; email?: string; password?: string; device_name?: string }
13
+
14
+ if (!body.name || !body.email || !body.password) {
15
+ return MantiqResponse.json({ error: 'Name, email and password are required.' }, 422)
16
+ }
17
+ if (body.password.length < 6) {
18
+ return MantiqResponse.json({ error: 'Password must be at least 6 characters.' }, 422)
19
+ }
20
+
21
+ const existing = await User.where('email', body.email).first()
22
+ if (existing) {
23
+ return MantiqResponse.json({ error: 'A user with this email already exists.' }, 422)
24
+ }
25
+
26
+ const hasher = new HashManager({ bcrypt: { rounds: 10 } })
27
+ const user = await User.create({
28
+ name: body.name,
29
+ email: body.email,
30
+ password: await hasher.make(body.password),
31
+ })
32
+
33
+ const { plainTextToken } = await (user as any).createToken(
34
+ body.device_name ?? 'api',
35
+ )
36
+
37
+ return MantiqResponse.json({
38
+ message: 'Registered.',
39
+ user: user.toObject(),
40
+ token: plainTextToken,
41
+ }, 201)
42
+ }
43
+
44
+ async login(request: MantiqRequest): Promise<Response> {
45
+ const body = await request.input() as { email?: string; password?: string; device_name?: string }
46
+
47
+ if (!body.email || !body.password) {
48
+ return MantiqResponse.json({ error: 'Email and password are required.' }, 422)
49
+ }
50
+
51
+ const user = await User.where('email', body.email).first()
52
+ if (!user) {
53
+ return MantiqResponse.json({ error: 'Invalid credentials.' }, 401)
54
+ }
55
+
56
+ const hasher = new HashManager({ bcrypt: { rounds: 10 } })
57
+ const valid = await hasher.check(body.password, user.getAuthPassword())
58
+ if (!valid) {
59
+ return MantiqResponse.json({ error: 'Invalid credentials.' }, 401)
60
+ }
61
+
62
+ const { plainTextToken } = await (user as any).createToken(
63
+ body.device_name ?? 'api',
64
+ )
65
+
66
+ return MantiqResponse.json({
67
+ message: 'Logged in.',
68
+ user: user.toObject(),
69
+ token: plainTextToken,
70
+ })
71
+ }
72
+
73
+ async logout(request: MantiqRequest): Promise<Response> {
74
+ const manager = auth()
75
+ manager.setRequest(request)
76
+ const user = await manager.guard('api').user()
77
+
78
+ if (user) {
79
+ // Revoke the current token
80
+ const token = (user as any).currentAccessToken?.()
81
+ if (token) await token.delete()
82
+ }
83
+
84
+ return MantiqResponse.json({ message: 'Token revoked.' })
85
+ }
86
+
87
+ async user(request: MantiqRequest): Promise<Response> {
88
+ const manager = auth()
89
+ manager.setRequest(request)
90
+ const user = await manager.guard('api').user()
91
+
92
+ if (!user) {
93
+ return MantiqResponse.json({ error: 'Unauthenticated.' }, 401)
94
+ }
95
+
96
+ return MantiqResponse.json({ user: (user as any).toObject() })
97
+ }
98
+
99
+ async tokens(request: MantiqRequest): Promise<Response> {
100
+ const manager = auth()
101
+ manager.setRequest(request)
102
+ const user = await manager.guard('api').user()
103
+
104
+ if (!user) {
105
+ return MantiqResponse.json({ error: 'Unauthenticated.' }, 401)
106
+ }
107
+
108
+ const tokens = await (user as any).tokens().get()
109
+ return MantiqResponse.json({
110
+ data: tokens.map((t: any) => ({
111
+ id: t.getKey(),
112
+ name: t.getAttribute('name'),
113
+ abilities: JSON.parse(t.getAttribute('abilities') ?? '["*"]'),
114
+ last_used_at: t.getAttribute('last_used_at'),
115
+ created_at: t.getAttribute('created_at'),
116
+ })),
117
+ })
118
+ }
119
+ }
@@ -13,8 +13,8 @@
13
13
  [key: string]: any
14
14
  } = $props()
15
15
 
16
- let email = $state('admin@example.com')
17
- let password = $state('password')
16
+ let email = $state('')
17
+ let password = $state('')
18
18
  let error = $state('')
19
19
  let loading = $state(false)
20
20
 
@@ -82,7 +82,7 @@
82
82
  type="email"
83
83
  bind:value={email}
84
84
  required
85
- placeholder="admin@example.com"
85
+ placeholder="you@example.com"
86
86
  autocomplete="email"
87
87
  />
88
88
  </div>
@@ -12,8 +12,8 @@ const props = withDefaults(defineProps<{
12
12
  appName: 'Mantiq',
13
13
  })
14
14
 
15
- const email = ref('admin@example.com')
16
- const password = ref('password')
15
+ const email = ref('')
16
+ const password = ref('')
17
17
  const error = ref('')
18
18
  const loading = ref(false)
19
19
 
@@ -82,7 +82,7 @@ async function handleSubmit() {
82
82
  v-model="email"
83
83
  type="email"
84
84
  required
85
- placeholder="admin@example.com"
85
+ placeholder="you@example.com"
86
86
  autocomplete="email"
87
87
  />
88
88
  </div>