Skip to content

Commit c5f3dfc

Browse files
committed
Updated backend to TypeScript
1 parent e512dc9 commit c5f3dfc

39 files changed

+2891
-2307
lines changed

.vscode/extensions.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

.vscode/launch.json

Lines changed: 0 additions & 11 deletions
This file was deleted.

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ RUN npx prisma generate
2626
RUN apk add --no-cache git
2727

2828
EXPOSE 3000
29-
CMD npx prisma db push && node server.js
29+
CMD npx prisma db push && npx tsx backend/server.ts

backend/configs/app.config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { exit } from "process";
2+
3+
export const PORT = 3000;
4+
if (!process.env.JWT_SECRET) {
5+
console.error("JWT_SECRET is not set");
6+
exit(1);
7+
}
8+
export const JWT_SECRET = process.env.JWT_SECRET;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Request, Response } from "express";
2+
import { AccessTokenService } from "../services/accessToken.service";
3+
import { AuthenticatedRequest } from "../types/AuthenticatedRequest";
4+
5+
export async function getAccessTokens(req: Request, res: Response) {
6+
const username = (req as AuthenticatedRequest).user.username;
7+
const accessTokens = await AccessTokenService.getAccessToken(username);
8+
9+
return res.json(accessTokens);
10+
}
11+
12+
export async function createAccessToken(req: Request, res: Response) {
13+
const { name, token } = req.body;
14+
const username = (req as AuthenticatedRequest).user.username;
15+
16+
const accessToken = await AccessTokenService.createAccessToken(
17+
name,
18+
token,
19+
username
20+
);
21+
22+
return res.json(accessToken);
23+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Logger from "../lib/logger";
2+
import { ConfigService } from "../services/config.service";
3+
import SambaClient from "samba-client";
4+
import { Request, Response } from "express";
5+
6+
const logger = new Logger("config.controller");
7+
8+
export async function getStorageConfig(req: Request, res: Response) {
9+
const storageConfig = await ConfigService.getStorageConfig();
10+
11+
return res.json(storageConfig);
12+
}
13+
14+
export async function updateStorageConfig(req: Request, res: Response) {
15+
const { location, serverAddress, remoteLocation, username, password } =
16+
req.body;
17+
18+
try {
19+
if (location === "smb_share") {
20+
if (!serverAddress || !remoteLocation || !username || !password) {
21+
return res.status(400).json({ error: "Missing required fields" });
22+
}
23+
24+
const client = new SambaClient({
25+
address: `//${serverAddress}`,
26+
username,
27+
password,
28+
});
29+
30+
// Test connection
31+
await client.listFiles("", "");
32+
33+
// Update configurations
34+
await Promise.all([
35+
ConfigService.updateConfigEntry("default_location", "smb_share"),
36+
ConfigService.updateConfigEntry("smb_address", serverAddress),
37+
ConfigService.updateConfigEntry("smb_location", remoteLocation),
38+
ConfigService.updateConfigEntry("smb_username", username),
39+
ConfigService.updateConfigEntry("smb_password", password),
40+
]);
41+
} else if (location === "local_folder") {
42+
await ConfigService.updateConfigEntry("default_location", "local_folder");
43+
}
44+
45+
res.json({ message: "Setting saved" });
46+
} catch (error) {
47+
logger.error(`Error in storage configuration: ${error.message}`);
48+
res.status(500).json({
49+
error:
50+
"Error connecting to storage. Please check the wiki on GitHub for more information.",
51+
details:
52+
"https://github.com/TimWitzdam/GitSave/wiki/How-to-set-up-SMB-share",
53+
});
54+
}
55+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { HistoryService } from "../services/history.service";
2+
import { Request, Response } from "express";
3+
import { AuthenticatedRequest } from "../types/AuthenticatedRequest";
4+
5+
interface PaginationQuery {
6+
limit?: string;
7+
offset?: string;
8+
}
9+
10+
export async function getHistory(
11+
req: Request<{}, {}, {}, PaginationQuery>,
12+
res: Response
13+
) {
14+
const { limit, offset } = req.query;
15+
const username = (req as AuthenticatedRequest).user.username;
16+
17+
// Convert string values to numbers
18+
const numLimit = limit ? parseInt(limit, 10) : undefined;
19+
const numOffset = offset ? parseInt(offset, 10) : undefined;
20+
21+
// Check if values are missing or invalid
22+
if (
23+
numLimit === undefined ||
24+
numOffset === undefined ||
25+
isNaN(numLimit) ||
26+
isNaN(numOffset) ||
27+
numLimit <= 0 ||
28+
numOffset < 0
29+
) {
30+
return res.status(400).send("Invalid limit or offset");
31+
}
32+
33+
const { backupHistory, totalCount } = await HistoryService.getHistory(
34+
username,
35+
numOffset,
36+
numLimit
37+
);
38+
return res.json({ backupHistory, totalCount });
39+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { ScheduleService } from "../services/schedule.service";
2+
import { execFile } from "child_process";
3+
import Logger from "../lib/logger";
4+
import {
5+
createBackup,
6+
resumeCronJob,
7+
scheduleCronJobs,
8+
stopCronJob,
9+
} from "../server";
10+
import { Request, Response } from "express";
11+
import { AuthenticatedRequest } from "../types/AuthenticatedRequest";
12+
13+
const logger = new Logger("schedule.controller");
14+
15+
export async function getSchedules(req: Request, res: Response) {
16+
const username = (req as AuthenticatedRequest).user.username;
17+
const schedules = await ScheduleService.getSchedulesByUser(username);
18+
return res.json(schedules);
19+
}
20+
21+
export async function createSchedule(req: Request, res: Response) {
22+
const schedule = req.body;
23+
const username = (req as AuthenticatedRequest).user.username;
24+
25+
let repoUrl: URL;
26+
try {
27+
repoUrl = new URL(schedule.repository);
28+
} catch (error) {
29+
return res.status(400).send("Invalid repository URL");
30+
}
31+
32+
const initialUrl = repoUrl.href;
33+
if (schedule.private) {
34+
const accessToken = await ScheduleService.getAccessToken(
35+
schedule.accessTokenId,
36+
);
37+
if (!accessToken) {
38+
return res.status(400).send("Access token not found");
39+
}
40+
41+
repoUrl.href = repoUrl.href.replace(
42+
"https://",
43+
`https://${accessToken.token}@`,
44+
);
45+
}
46+
47+
const options = {
48+
env: {
49+
...process.env,
50+
GIT_ASKPASS: "/bin/false",
51+
},
52+
};
53+
const child = execFile(
54+
"git",
55+
["ls-remote", repoUrl.href],
56+
options,
57+
async (error, stdout, stderr) => {
58+
if (error) {
59+
logger.error(error.toString());
60+
return res
61+
.status(400)
62+
.send(
63+
"Invalid repository. Either it does not exist or you forgot to select an access token.",
64+
);
65+
}
66+
67+
const newSchedule = await ScheduleService.addSchedule(
68+
username,
69+
schedule,
70+
initialUrl,
71+
);
72+
scheduleCronJobs();
73+
return res.json(newSchedule);
74+
},
75+
);
76+
77+
const timeout = setTimeout(() => {
78+
child.kill();
79+
}, 5000);
80+
81+
child.on("exit", (code) => {
82+
clearTimeout(timeout);
83+
if (code === null) {
84+
return res.status(400).send("The process took too long and was aborted.");
85+
}
86+
});
87+
}
88+
89+
export async function updateSchedule(req: Request, res: Response) {
90+
const { id } = req.params;
91+
const schedule = req.body;
92+
93+
let url: URL;
94+
95+
try {
96+
url = new URL(schedule.repository);
97+
} catch (error) {
98+
return res.status(400).send("Invalid repository URL");
99+
}
100+
101+
const options = {
102+
env: {
103+
...process.env,
104+
GIT_ASKPASS: "/bin/false",
105+
},
106+
};
107+
const child = execFile(
108+
"git",
109+
["ls-remote", url.href],
110+
options,
111+
async (error, stdout, stderr) => {
112+
if (error) {
113+
logger.error(error.toString());
114+
return res
115+
.status(400)
116+
.send(
117+
"Invalid repository. Either it does not exist or you forgot to select an access token.",
118+
);
119+
}
120+
121+
const updatedSchedule = await ScheduleService.editSchedule(schedule, id);
122+
scheduleCronJobs();
123+
return res.json(updatedSchedule);
124+
},
125+
);
126+
127+
const timeout = setTimeout(() => {
128+
child.kill();
129+
}, 5000);
130+
131+
child.on("exit", (code) => {
132+
clearTimeout(timeout);
133+
if (code === null) {
134+
return res.status(400).send("The process took too long and was aborted.");
135+
}
136+
});
137+
}
138+
139+
export async function backupScheduleNow(req: Request, res: Response) {
140+
const { id } = req.params;
141+
142+
const schedule = await ScheduleService.getScheduleById(id);
143+
144+
if (!schedule) {
145+
return res.status(404).send("Schedule not found");
146+
}
147+
148+
createBackup(schedule.id, schedule.name, schedule.repository);
149+
return res.send(
150+
"Backup started. Depending on the size of the repository, this may take a while.",
151+
);
152+
}
153+
154+
export async function pauseSchedule(req: Request, res: Response) {
155+
const { id } = req.params;
156+
157+
const pausedSchedule = await ScheduleService.pauseSchedule(id);
158+
stopCronJob(parseInt(id));
159+
return res.json(pausedSchedule);
160+
}
161+
162+
export async function resumeSchedule(req: Request, res: Response) {
163+
const { id } = req.params;
164+
165+
const resumedSchedule = await ScheduleService.resumeSchedule(id);
166+
resumeCronJob(parseInt(id));
167+
return res.json(resumedSchedule);
168+
}
169+
170+
export async function deleteSchedule(req: Request, res: Response) {
171+
const { id } = req.params;
172+
173+
await ScheduleService.deleteSchedule(id);
174+
stopCronJob(parseInt(id), true);
175+
return res.send("Schedule deleted");
176+
}

0 commit comments

Comments
 (0)