mirror of
https://github.com/Ravinou/borgwarehouse
synced 2026-03-14 14:25:46 +01:00
feat: ✨ new account setup API
This commit is contained in:
parent
4e7b880624
commit
ec17ad19d6
5 changed files with 306 additions and 2 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -114,4 +114,5 @@ config/users.json
|
|||
docker-compose.yml
|
||||
|
||||
# Commit tests docker-compose
|
||||
!tests/bats/docker-compose.yml
|
||||
!tests/bats/docker-compose.yml
|
||||
certificates
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
|
|
@ -8392,6 +8392,15 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
|
||||
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
222
pages/api/v1/account/setup.test.ts
Normal file
222
pages/api/v1/account/setup.test.ts
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
import { createMocks } from 'node-mocks-http';
|
||||
import handler from './setup';
|
||||
import { AuthService, ConfigService } from '~/services';
|
||||
import { BorgWarehouseUser, Repository } from '~/types';
|
||||
|
||||
vi.mock('~/services');
|
||||
|
||||
const bwUserMock: BorgWarehouseUser = {
|
||||
id: 0,
|
||||
username: 'testuser',
|
||||
password: 'hashedpassword',
|
||||
email: 'test@test.fr',
|
||||
roles: ['admin'],
|
||||
};
|
||||
|
||||
const repoListMock: Repository[] = [
|
||||
{
|
||||
id: 1,
|
||||
alias: 'repo1',
|
||||
repositoryName: 'Repository 1',
|
||||
status: true,
|
||||
lastSave: 0,
|
||||
storageSize: 1000,
|
||||
storageUsed: 500,
|
||||
sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey1',
|
||||
comment: 'First repository',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
alias: 'repo2',
|
||||
repositoryName: 'Repository 2',
|
||||
status: false,
|
||||
lastSave: 0,
|
||||
storageSize: 2000,
|
||||
storageUsed: 1500,
|
||||
sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey2',
|
||||
comment: 'Second repository',
|
||||
},
|
||||
];
|
||||
|
||||
describe('API /api/v1/account/setup', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should return 404 if DISABLE_AUTO_SETUP is true', async () => {
|
||||
process.env.DISABLE_AUTO_SETUP = 'true';
|
||||
|
||||
const { req, res } = createMocks({ method: 'GET' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(404);
|
||||
delete process.env.DISABLE_AUTO_SETUP;
|
||||
});
|
||||
|
||||
it('should return 405 if method is not GET or POST', async () => {
|
||||
const { req, res } = createMocks({ method: 'PUT' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(405);
|
||||
});
|
||||
|
||||
it('should return setup: false if users exist on GET', async () => {
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([bwUserMock]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
|
||||
const { req, res } = createMocks({ method: 'GET' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({ setup: false });
|
||||
delete process.env.DISABLE_AUTO_SETUP;
|
||||
});
|
||||
|
||||
it('should return forbidden if repos exist but no users on GET', async () => {
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue(repoListMock);
|
||||
|
||||
const { req, res } = createMocks({ method: 'GET' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(403);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({
|
||||
status: 403,
|
||||
message:
|
||||
'The setup has already been completed, and some repositories are already configured. To reset, please delete the config/repo.json file or rebuild/restore the config/users.json file.',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return setup: true if no users or repos exist on GET', async () => {
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
|
||||
const { req, res } = createMocks({ method: 'GET' });
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({ setup: true });
|
||||
});
|
||||
|
||||
it('should return 422 if username is invalid', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
body: { username: 'invalid username byebye', password: 'bigbigpassword' },
|
||||
});
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(422);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({
|
||||
error: 'Only a-z characters are allowed (1 to 20 char.)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 422 if password is too short', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
body: { username: 'validuser', password: 'short' },
|
||||
});
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(422);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({
|
||||
error: 'Password must be at least 12 characters long',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 422 if email is invalid', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
body: { username: 'validuser', password: 'validpassword123', email: 'invalidemail' },
|
||||
});
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
await handler(req, res);
|
||||
expect(res._getStatusCode()).toBe(422);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({
|
||||
error: 'Invalid email address',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if users or repos already exist on POST', async () => {
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([bwUserMock]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
});
|
||||
await handler(req, res);
|
||||
|
||||
expect(res._getStatusCode()).toBe(404);
|
||||
});
|
||||
|
||||
it('should complete setup successfully on valid POST', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: 'validuser',
|
||||
password: 'validpassword123',
|
||||
email: 'user@example.com',
|
||||
},
|
||||
});
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
vi.mocked(AuthService.hashPassword).mockResolvedValue('hashedpassword');
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 0,
|
||||
username: 'validuser',
|
||||
password: 'hashedpassword',
|
||||
email: 'user@example.com',
|
||||
roles: ['admin'],
|
||||
},
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({ status: 200, message: 'Setup completed successfully' });
|
||||
});
|
||||
|
||||
it('should complete setup successfully without email on valid POST', async () => {
|
||||
const { req, res } = createMocks({
|
||||
method: 'POST',
|
||||
body: {
|
||||
username: 'validuser',
|
||||
password: 'validpassword123',
|
||||
},
|
||||
});
|
||||
vi.mocked(ConfigService.getUsersList).mockResolvedValue([]);
|
||||
vi.mocked(ConfigService.getRepoList).mockResolvedValue([]);
|
||||
vi.mocked(AuthService.hashPassword).mockResolvedValue('hashedpassword');
|
||||
|
||||
await handler(req, res);
|
||||
|
||||
expect(ConfigService.updateUsersList).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 0,
|
||||
username: 'validuser',
|
||||
password: 'hashedpassword',
|
||||
roles: ['admin'],
|
||||
},
|
||||
]);
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
const responseData = JSON.parse(res._getData());
|
||||
expect(responseData).toEqual({ status: 200, message: 'Setup completed successfully' });
|
||||
});
|
||||
});
|
||||
71
pages/api/v1/account/setup.ts
Normal file
71
pages/api/v1/account/setup.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { AuthService, ConfigService } from '~/services';
|
||||
import { z } from 'zod';
|
||||
import ApiResponse from '~/helpers/functions/apiResponse';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (process.env.DISABLE_AUTO_SETUP === 'true') {
|
||||
return ApiResponse.notFound(res);
|
||||
}
|
||||
if (req.method !== 'GET' && req.method !== 'POST') {
|
||||
return ApiResponse.methodNotAllowed(res);
|
||||
}
|
||||
|
||||
const repoList = await ConfigService.getRepoList();
|
||||
const usersList = await ConfigService.getUsersList();
|
||||
|
||||
const hasUsers = usersList.length > 0;
|
||||
const hasRepos = repoList.length > 0;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
try {
|
||||
if (hasUsers) {
|
||||
return res.status(200).json({ setup: false });
|
||||
}
|
||||
if (!hasUsers && hasRepos) {
|
||||
return ApiResponse.forbidden(
|
||||
res,
|
||||
'The setup has already been completed, and some repositories are already configured. To reset, please delete the config/repo.json file or rebuild/restore the config/users.json file.'
|
||||
);
|
||||
}
|
||||
return res.status(200).json({ setup: true });
|
||||
} catch (error) {
|
||||
return ApiResponse.serverError(res, error);
|
||||
}
|
||||
} else if (req.method === 'POST') {
|
||||
try {
|
||||
if (hasUsers || hasRepos) {
|
||||
return ApiResponse.notFound(res);
|
||||
}
|
||||
|
||||
const setupSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^[a-z]{1,20}$/, 'Only a-z characters are allowed (1 to 20 char.)'),
|
||||
email: z.string().trim().email('Invalid email address').optional(),
|
||||
password: z.string().min(12, 'Password must be at least 12 characters long'),
|
||||
});
|
||||
const parsedData = setupSchema.safeParse(req.body);
|
||||
if (!parsedData.success) {
|
||||
return res.status(422).json({ error: parsedData.error.errors[0].message });
|
||||
}
|
||||
|
||||
const hashedPassword = await AuthService.hashPassword(parsedData.data.password);
|
||||
|
||||
await ConfigService.updateUsersList([
|
||||
{
|
||||
id: 0,
|
||||
username: parsedData.data.username,
|
||||
password: hashedPassword,
|
||||
email: parsedData.data.email,
|
||||
roles: ['admin'],
|
||||
},
|
||||
]);
|
||||
|
||||
return ApiResponse.success(res, 'Setup completed successfully');
|
||||
} catch (error) {
|
||||
return ApiResponse.serverError(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,8 @@ export type BorgWarehouseUser = {
|
|||
username: string;
|
||||
password: string;
|
||||
roles: string[];
|
||||
email: string;
|
||||
email?: string;
|
||||
oauth_emails?: string[];
|
||||
emailAlert?: boolean;
|
||||
appriseAlert?: boolean;
|
||||
appriseMode?: AppriseModeEnum;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue