This commit is contained in:
Feror 2025-03-12 08:17:34 +01:00
commit b96ed99cc4
16 changed files with 620 additions and 0 deletions

34
.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

15
README.md Normal file
View file

@ -0,0 +1,15 @@
# bun-ssh-portfolio
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.5. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

62
bun.lock Normal file
View file

@ -0,0 +1,62 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bun-ssh-portfolio",
"dependencies": {
"@types/ssh2": "^1.15.4",
"ansi-escapes": "^7.0.0",
"ssh2": "^1.16.0",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
"@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="],
"@types/ssh2": ["@types/ssh2@1.15.4", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-9JTQgVBWSgq6mAen6PVnrAmty1lqgCMvpfN+1Ck5WRUsyMYPa6qd50/vMJ0y1zkGpOEgLzm8m8Dx/Y5vRouLaA=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"ansi-escapes": ["ansi-escapes@7.0.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw=="],
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
"buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
"bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="],
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@types/ws/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
"bun-types/@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="],
"@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
"bun-types/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
}
}

33
contact/ContactForm.ts Normal file
View file

@ -0,0 +1,33 @@
import { newLine } from "../front";
interface ContactFormProps {
columns: number;
rows: number;
contactFormData: {
name: string;
email: string;
message: string;
};
contactFormSelectedField: "name" | "email" | "message" | "submit";
}
const ContactForm = ({ rows, columns, contactFormData, contactFormSelectedField }: ContactFormProps) => {
return (
`${newLine}` +
`Send your message to Jokin here.${newLine}` +
`You can switch between fields with the tab key.${newLine}` +
`Then, send the message by selecting the "Submit" button and hitting enter.${newLine}${newLine}` +
` ${contactFormSelectedField == "name" ? "\x1b[7mYour name\x1b[0m" : "Your name"}: ${contactFormData.name}${
contactFormSelectedField == "name" ? "|" : ""
}${newLine}` +
` ${contactFormSelectedField == "email" ? "\x1b[7mYour email\x1b[0m" : "Your email"}: ${
contactFormData.email
}${contactFormSelectedField == "email" ? "|" : ""}${newLine}` +
` ${contactFormSelectedField == "message" ? "\x1b[7mYour message\x1b[0m" : "Your message"}:${newLine} ${
contactFormData.message
}${contactFormSelectedField == "message" ? "|" : ""}${newLine}` +
`${newLine}` +
` ${contactFormSelectedField == "submit" ? "\x1b[7mSubmit\x1b[0m" : "Submit"}`
);
};
export default ContactForm;

20
front/Categories.ts Normal file
View file

@ -0,0 +1,20 @@
import { newLine } from ".";
interface CategoriesProps {
width: number;
height: 3;
selectedTab: "About" | "Projects" | "Contact";
}
const Categories = ({ width, height, selectedTab }: CategoriesProps) => {
const categories = ["🇦 about", "🇵 projects", "🇨 contact", "🇶 quit"];
const separatorSpaces = " ".repeat(width / 2 - categories.join(" | ").length / 2 - 4);
const separatorLine = "─".repeat(width - separatorSpaces.length * 2 - 6 - (width % 2 ? 0 : 1));
return (
`${separatorSpaces}${categories
.map((value) => (value.includes(selectedTab.toLowerCase()) ? `\x1b[7m${value}\x1b[0m` : value))
.join(" | ")} ${separatorSpaces}${newLine}` + `${separatorSpaces}${separatorLine}${separatorSpaces}`
);
};
export default Categories;

20
front/Header.ts Normal file
View file

@ -0,0 +1,20 @@
import { newLine } from ".";
interface HeaderProps {
width: number;
height: 3;
}
const Header = ({ width, height }: HeaderProps) => {
const title = "🔥 Welcome to My Portfolio CLI 🔥";
const separator = "─".repeat(width / 2 - title.length / 2);
const subTitle = "Made by: Jokin Suares";
const subTitleSeparator = " ".repeat(width / 2 - subTitle.length / 2);
return (
`${separator}${title}${separator}${newLine}` +
`${subTitleSeparator}${subTitle}${subTitleSeparator}${newLine}` +
`${"─".repeat(width)}`
);
};
export default Header;

9
front/Tabs/About.ts Normal file
View file

@ -0,0 +1,9 @@
interface AboutProps {
width: number;
height: number;
}
const About = ({ width, height }: AboutProps) => {
return "About";
};
export default About;

20
front/Tabs/Contact.ts Normal file
View file

@ -0,0 +1,20 @@
import { newLine } from "..";
import { link } from "ansi-escapes";
interface ContactProps {
width: number;
height: number;
}
const Contact = ({ width, height }: ContactProps) => {
return (
`${newLine}` +
` Email: ${link("contact@jokinsuares.fr", "mailto:contact@jokinsuares.fr")}${newLine}` +
` Phone: ${link("+33 6 02 26 22 00", "tel:+33602262200")}${newLine}` +
` GitHub: ${link("Feror-BotMaker", "https://github.com/Feror-BotMaker")}${newLine}` +
` Discord: ${link("Feror", "https://discordapp.com/users/323206956640763916")}${newLine}` +
` LinkedIn: ${link("Jokin Suares", "https://www.linkedin.com/in/jokin-suares/")}${newLine}` +
` ssh: $ ssh contact@jokinsuares.fr${newLine}`
);
};
export default Contact;

9
front/Tabs/Projects.ts Normal file
View file

@ -0,0 +1,9 @@
interface ProjectsProps {
width: number;
height: number;
}
const Projects = ({ width, height }: ProjectsProps) => {
return "Projects";
};
export default Projects;

21
front/Tabs/TabRouter.ts Normal file
View file

@ -0,0 +1,21 @@
import About from "./About";
import Contact from "./Contact";
import Projects from "./Projects";
interface TabRouterProps {
width: number;
height: number;
selectedTab: "About" | "Projects" | "Contact";
}
const TabRouter = ({ width, height, selectedTab }: TabRouterProps) => {
switch (selectedTab) {
case "About":
return About({ width, height });
case "Projects":
return Projects({ width, height });
case "Contact":
return Contact({ width, height });
}
};
export default TabRouter;

34
front/index.ts Normal file
View file

@ -0,0 +1,34 @@
import Categories from "./Categories";
import Header from "./Header";
import TabRouter from "./Tabs/TabRouter";
interface FrontProps {
columns: number;
rows: number;
selectedTab: "About" | "Projects" | "Contact";
}
const terminalTooSmall = (width: number, height: number) => {
return (
`Oh no! Your terminal window is too small!${newLine}` +
`Minimum size required: 60x20` +
`Current size: ${width}x${height}`
);
};
const Front = ({ columns, rows, selectedTab }: FrontProps) => {
if (columns < 60 || rows < 20) {
return terminalTooSmall(columns, rows);
}
return (
`${Header({ width: columns, height: 3 })}${newLine}` +
`${Categories({ width: columns, height: 3, selectedTab })}${newLine}` +
`${TabRouter({ width: columns, height: 3, selectedTab })}`
);
};
const newLine = "\n\r";
export default Front;
export { newLine };

38
host.key Normal file
View file

@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

1
host.key.pub Normal file
View file

@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDPXG3OLEYlb53XPbL5cqPpGby+2XIgKc1eIbi/o1XOfAKLvxO7Kwl86s/Uqz4bZwXGqK3N3/QytjXtyg8fsYNasBjAZos3BwX0VkUpdmOkh62jeKG8au6ftX0QC/5DHF4jv6/tFaH3tyLdhAVj2J4XnY2O2u6f3MVWJb8lFxDfhqzED9wggAP96RRwXpnbBV82JGcXhs1i8NPlJuTYMrYAEqwlayMZ7L48rTL7JMRHqryGVf5DZft+4B43f5r/9JUei+qSeJzgyCeM1rK9jT9DukDjFaUA/xS3HeRSj1slyd8iMrbYid8Mu4XR6rYWeZTxjz7D/FXtVyO81/EvVLpVoWINbdeelWo9KiyaCjPsO7PR+mOncfkewxOmau7iCXXdR13eQSJIJ+7DAK4ZF6IUltzjotRrRaMNKPNrthgYoEBE7GYrzsjpCaMqxcj4i2e4P1U495a3t9umv4BW7k12Uu2lsneAYP0xyYxYx4/d81/uyu0+f5/iYKdkZ32MX9c= feror@MBP-de-Feror.local

259
index.ts Normal file
View file

@ -0,0 +1,259 @@
import { Server, type ServerChannel } from "ssh2";
import Front, { newLine } from "./front";
import { eraseScreen, cursorTo, cursorHide, cursorShow } from "ansi-escapes";
import ContactForm from "./contact/ContactForm";
const inputCodes = {
int8: {
backspace: 127,
tab: 9,
enter: 13,
space: 32,
escape: 27,
ctrlC: 3,
a: 97,
p: 112,
c: 99,
q: 113,
},
base64: {
arrows: {
up: "G1tB",
down: "G1tC",
right: "G1tD",
left: "G1tE",
},
},
};
const server = new Server(
{
hostKeys: [await Bun.file("./host.key").text()],
ident: "portfolio.sh",
},
(client) => {
console.log("Client connected!");
const userSpecs: {
userName?: string;
} = {};
client
.on("authentication", (ctx) => {
userSpecs.userName = ctx.username;
ctx.accept(); // On accepte toutes les connexions
})
.on("ready", () => {
client.on("session", (accept) => {
const session = accept();
const userWindow: {
columns: number;
rows: number;
selectedTab: "About" | "Projects" | "Contact";
stream: ServerChannel | null;
contactFormData: {
name: string;
email: string;
message: string;
};
contactFormSelectedField: "name" | "email" | "message" | "submit";
} = {
columns: 80,
rows: 24,
selectedTab: "About",
stream: null,
contactFormData: {
name: "",
email: "",
message: "",
},
contactFormSelectedField: "name",
};
const render = () => {
userWindow.stream?.write(eraseScreen);
userWindow.stream?.write(cursorTo(0, 0));
userWindow.stream?.write(cursorHide);
if (userSpecs.userName === "contact") {
userWindow.stream?.write(ContactForm(userWindow));
return;
}
userWindow.stream?.write(Front(userWindow));
};
session.on("pty", (accept, reject, info) => {
console.log(info);
userWindow.columns = info.cols;
userWindow.rows = info.rows;
accept();
});
session.on("window-change", (_accept, _reject, info) => {
console.log(info);
userWindow.columns = info.cols;
userWindow.rows = info.rows;
render();
});
session.on("shell", (accept) => {
const stream = accept();
// stream.write(`
// 🔥 Welcome to My Portfolio CLI 🔥
// ------------------------------
// Commands: about | projects | contact | exit
// `);
userWindow.stream = stream;
render();
stream.on("data", (data: Buffer) => {
const cmd = data.toString();
const cmdAsInt = data.readInt8();
if (
cmdAsInt == inputCodes.int8.ctrlC ||
(cmdAsInt === inputCodes.int8.q && userSpecs.userName !== "contact")
) {
userWindow.stream?.write(eraseScreen);
userWindow.stream?.write(cursorTo(0, 0));
stream.write("Goodbye!\n");
stream.write(cursorShow);
stream.close();
}
if (userSpecs.userName === "contact") {
console.log(cmdAsInt);
if (cmdAsInt == inputCodes.int8.tab) {
if (userWindow.contactFormSelectedField === "name") {
userWindow.contactFormSelectedField = "email";
} else if (userWindow.contactFormSelectedField === "email") {
userWindow.contactFormSelectedField = "message";
} else if (userWindow.contactFormSelectedField === "message") {
userWindow.contactFormSelectedField = "submit";
} else if (userWindow.contactFormSelectedField === "submit") {
userWindow.contactFormSelectedField = "name";
}
render();
}
if (cmdAsInt == inputCodes.int8.backspace) {
if (userWindow.contactFormSelectedField === "name") {
userWindow.contactFormData.name = userWindow.contactFormData.name.slice(0, -1);
} else if (userWindow.contactFormSelectedField === "email") {
userWindow.contactFormData.email = userWindow.contactFormData.email.slice(
0,
-1
);
} else if (userWindow.contactFormSelectedField === "message") {
userWindow.contactFormData.message = userWindow.contactFormData.message.slice(
0,
-1
);
}
render();
}
if (cmdAsInt == inputCodes.int8.space) {
if (userWindow.contactFormSelectedField === "name") {
userWindow.contactFormData.name += " ";
} else if (userWindow.contactFormSelectedField === "email") {
userWindow.contactFormData.email += " ";
} else if (userWindow.contactFormSelectedField === "message") {
userWindow.contactFormData.message += " ";
}
render();
}
if (cmdAsInt == inputCodes.int8.enter) {
if (userWindow.contactFormSelectedField === "message") {
userWindow.contactFormData.message += newLine;
} else if (userWindow.contactFormSelectedField === "submit") {
const webhookUrl =
"https://discord.com/api/webhooks/1349123668914999361/2rvvgNNRyzgGpKFJ0JLnT-PbkbM-phyQZwTxRBkxDWSA_9SybsP2eILbxsegthsWsu8d";
const messageData = {
content: `<@323206956640763916>\nName: ${
userWindow.contactFormData.name
}\nEmail: ${
userWindow.contactFormData.email
}\nMessage: ${userWindow.contactFormData.message.replace(newLine, "\n")}`,
};
fetch(webhookUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(messageData),
})
.then((response) => {
if (!response.ok) {
throw new Error("Failed to send message to Discord webhook");
}
console.log("Message sent to Discord webhook successfully");
})
.catch((error) => {
console.error("Error sending message to Discord webhook:", error);
});
userWindow.stream?.write(eraseScreen);
userWindow.stream?.write(cursorTo(0, 0));
stream.write(cursorShow);
userWindow.stream?.write("Message sent!\n");
stream.close();
}
render();
}
if (
cmdAsInt != inputCodes.int8.backspace &&
cmdAsInt != inputCodes.int8.escape &&
cmdAsInt != inputCodes.int8.tab &&
cmdAsInt != inputCodes.int8.space &&
cmdAsInt != inputCodes.int8.enter
) {
if (userWindow.contactFormSelectedField === "name") {
userWindow.contactFormData.name += cmd.trim();
} else if (userWindow.contactFormSelectedField === "email") {
userWindow.contactFormData.email += cmd.trim();
} else if (userWindow.contactFormSelectedField === "message") {
userWindow.contactFormData.message += cmd.trim();
}
render();
}
return;
}
console.log(cmd);
switch (cmdAsInt) {
case inputCodes.int8.a:
userWindow.selectedTab = "About";
render();
break;
case inputCodes.int8.p:
userWindow.selectedTab = "Projects";
render();
break;
case inputCodes.int8.c:
userWindow.selectedTab = "Contact";
render();
break;
}
});
});
});
});
client.on("end", () => {
console.log("Client disconnected");
});
}
);
server.listen(2222, "0.0.0.0", () => {
console.log("🚀 SSH server running on port 2222");
});

17
package.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "bun-ssh-portfolio",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"@types/ssh2": "^1.15.4",
"ansi-escapes": "^7.0.0",
"ssh2": "^1.16.0"
}
}

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["esnext"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}