create-mantiq 0.5.6 → 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,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)
|
package/skeleton/config/app.ts
CHANGED
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
|
+
}
|
|
@@ -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
|
+
}
|