first commit

This commit is contained in:
samuel 2022-06-03 19:06:56 +02:00
commit afdffcdfc0
37 changed files with 12764 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
*.conf

2
back/Makefile Normal file
View File

@ -0,0 +1,2 @@
build:
go build -o bin/server cmd/server/main.go

BIN
back/bin/server Executable file

Binary file not shown.

35
back/cmd/server/main.go Normal file
View File

@ -0,0 +1,35 @@
package main
import (
"flag"
"fmt"
"net/http"
"netoik.io/netoik-website/pkg/captcha"
"netoik.io/netoik-website/pkg/conf"
"netoik.io/netoik-website/pkg/contact"
"os"
)
func main() {
// Parse command line arguments
path := flag.String("c", "server.conf", "Config file")
flag.Parse()
// Parse config file
if !conf.ParseFile(*path) {
os.Exit(1)
}
// Setup captcha
captcha.Setup()
// Declare api routes
http.HandleFunc("/api/contact/send", contact.HandleSend)
http.HandleFunc("/api/captcha/new", captcha.HandleNew)
// Start listening
if err := http.ListenAndServe(fmt.Sprintf("%s:%d", conf.Conf.BindHost, conf.Conf.BindPort), nil); err != nil {
fmt.Fprintf(os.Stderr, "cannot listen at %s:%d: %s\n", conf.Conf.BindHost, conf.Conf.BindPort, err.Error())
os.Exit(1)
}
}

10
back/go.mod Normal file
View File

@ -0,0 +1,10 @@
module netoik.io/netoik-website
go 1.18
require (
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
)

8
back/go.sum Normal file
View File

@ -0,0 +1,8 @@
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f h1:q/DpyjJjZs94bziQ7YkBmIlpqbVP7yw179rnzoNVX1M=
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f/go.mod h1:QGrK8vMWWHQYQ3QU9bw9Y9OPNfxccGzfb41qjvVeXtY=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=

18
back/pkg/api/api.go Normal file
View File

@ -0,0 +1,18 @@
package api
import (
"encoding/json"
"net/http"
)
type Answer struct {
Success bool `json:"success"`
Id string `json:"id,omitempty"`
}
func Reply(w http.ResponseWriter, code int, answer Answer) {
a, _ := json.Marshal(answer)
w.Header().Set(http.CanonicalHeaderKey("content-type"), "application/json")
w.WriteHeader(code)
w.Write(a)
}

View File

@ -0,0 +1,60 @@
package captcha
import (
"errors"
"fmt"
"github.com/dchest/captcha"
"net/http"
"netoik.io/netoik-website/pkg/api"
"netoik.io/netoik-website/pkg/conf"
"os"
"path/filepath"
"time"
)
func writeImage(id string) bool {
path := filepath.Join(conf.Conf.CaptchaDirectory, id+".png")
file, err := os.Create(path)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: cannot write file %q: %s\n", path, err.Error())
return false
}
if err = captcha.WriteImage(file, id, conf.Conf.CaptchaWidth, conf.Conf.CaptchaHeight); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: cannot write captcha into file %q: %s\n", path, err.Error())
return false
}
return true
}
func HandleNew(w http.ResponseWriter, r *http.Request) {
// Check method
if r.Method != "POST" {
api.Reply(w, 405, api.Answer{})
return
}
// Create new captcha
id := captcha.NewLen(conf.Conf.CaptchaLength)
// Write captcha image
if !writeImage(id) {
api.Reply(w, 500, api.Answer{})
return
}
// Remove captcha image after expiration time
go func(id string) {
time.Sleep(conf.Conf.CaptchaExpiration)
path := filepath.Join(conf.Conf.CaptchaDirectory, id+".png")
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "ERROR: cannot remove captcha image after expiration %q: %s", path, err.Error())
}
}(id)
// Return captcha id
api.Reply(w, 200, api.Answer{Success: true, Id: id})
}
func Setup() {
captcha.SetCustomStore(captcha.NewMemoryStore(captcha.CollectNum, conf.Conf.CaptchaExpiration))
}

108
back/pkg/conf/conf.go Normal file
View File

@ -0,0 +1,108 @@
package conf
import (
"fmt"
"github.com/pelletier/go-toml"
"net"
"os"
"time"
)
type conf struct {
BindHost string `toml:"bind_host"`
BindPort int `toml:"bind_port"`
SMTPHost string `toml:"smtp_host"`
SMTPPort int `toml:"smtp_port"`
SMTPUsername string `toml:"smtp_username"`
SMTPPassword string `toml:"smtp_password"`
SMTPReceiver string `toml:"smtp_receiver"`
CaptchaDirectory string `toml:"captcha_directory"`
CaptchaLength int `toml:"captcha_length"`
CaptchaWidth int `toml:"captcha_width"`
CaptchaHeight int `toml:"captcha_height"`
CaptchaExpiration time.Duration `toml:"captcha_expiration"`
}
var Conf = conf{
BindHost: "127.0.0.1",
BindPort: 8000,
CaptchaLength: 6,
CaptchaWidth: 240,
CaptchaHeight: 80,
CaptchaExpiration: time.Hour,
}
func ParseFile(path string) bool {
var failure bool
// Open config file
file, err := os.Open(path)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: cannot open config file %q: %s\n", path, err.Error())
return false
}
// Read data from config file
var data = make([]byte, 10000)
n, err := file.Read(data)
if err != nil {
fmt.Fprintf(os.Stderr, "ERROR: cannot read config file %q: %s\n", path, err.Error())
return false
}
// Parse toml from config file
if err = toml.Unmarshal(data[:n], &Conf); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: cannot load toml config from file %q: %s\n", path, err.Error())
return false
}
// Check arguments in config file
if net.ParseIP(Conf.BindHost) == nil {
fmt.Fprintf(os.Stderr, "ERROR: bad value for 'bind_host' in config file %q: should be a valid IP address\n", path)
failure = true
}
if Conf.BindPort < 1 || Conf.BindPort > 65535 {
fmt.Fprintf(os.Stderr, "ERROR: bad value for 'bind_port' in config file %q: should be an integer in 1-65535\n", path)
failure = true
}
if Conf.SMTPHost == "" {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_host' in config file %q\n", path)
failure = true
}
if Conf.SMTPPort < 1 || Conf.SMTPPort > 65535 {
fmt.Fprintf(os.Stderr, "ERROR: bad value for 'smtp_port' in config file %q: should be an integer in 1-65535\n", path)
failure = true
}
if Conf.SMTPUsername == "" {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_username' in config file %q\n", path)
failure = true
}
if Conf.SMTPPassword == "" {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_password' in config file %q\n", path)
failure = true
}
if Conf.SMTPReceiver == "" {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'smtp_receiver' in config file %q\n", path)
failure = true
}
if Conf.CaptchaDirectory == "" {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_directory' in config file %q\n", path)
failure = true
}
if Conf.CaptchaLength == 0 {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_length' in config file %q\n", path)
failure = true
}
if Conf.CaptchaWidth == 0 {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_width' in config file %q\n", path)
failure = true
}
if Conf.CaptchaHeight == 0 {
fmt.Fprintf(os.Stderr, "ERROR: missing value for 'captcha_height' in config file %q\n", path)
failure = true
}
return !failure
}

View File

@ -0,0 +1,97 @@
package contact
import (
"encoding/json"
"errors"
"fmt"
"github.com/dchest/captcha"
"gopkg.in/gomail.v2"
"net/http"
"netoik.io/netoik-website/pkg/api"
"netoik.io/netoik-website/pkg/conf"
"os"
"path/filepath"
)
type request struct {
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
Company string `json:"company"`
Message string `json:"message"`
CaptchaId string `json:"captchaId"`
CaptchaDigits string `json:"captchaDigits"`
}
func HandleSend(w http.ResponseWriter, r *http.Request) {
// Check method
if r.Method != "POST" {
api.Reply(w, 405, api.Answer{})
return
}
// Parse json from request body
var data request
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
api.Reply(w, 400, api.Answer{})
return
}
if data.Name == "" || len(data.Name) > 200 {
api.Reply(w, 400, api.Answer{})
return
}
if data.Email == "" || len(data.Email) > 200 {
api.Reply(w, 400, api.Answer{})
return
}
if len(data.Phone) > 200 {
api.Reply(w, 400, api.Answer{})
return
}
if len(data.Company) > 200 {
api.Reply(w, 400, api.Answer{})
return
}
if data.Message == "" || len(data.Message) > 10000 {
api.Reply(w, 400, api.Answer{})
return
}
if data.CaptchaId == "" {
api.Reply(w, 400, api.Answer{})
return
}
if data.CaptchaDigits == "" {
api.Reply(w, 400, api.Answer{})
return
}
// Check captcha digits
if !captcha.VerifyString(data.CaptchaId, data.CaptchaDigits) {
api.Reply(w, 418, api.Answer{})
return
}
// Captcha has been verified, so remove image
path := filepath.Join(conf.Conf.CaptchaDirectory, data.CaptchaId+".png")
if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
fmt.Fprintf(os.Stderr, "ERROR: cannot remove captcha image %q: %s", path, err.Error())
}
// Build email
msg := gomail.NewMessage()
msg.SetHeader("From", conf.Conf.SMTPUsername)
msg.SetHeader("To", conf.Conf.SMTPReceiver)
msg.SetHeader("Subject", "Message from www.netoik.io")
msg.SetBody("text/plain", fmt.Sprintf(
"You have received a message from frontend.\nname: %s\nemail: %s\nphone: %s\ncompany: %s\n%s",
data.Name, data.Email, data.Phone, data.Company, data.Message))
// Configure SMTP dialer and send email
dialer := gomail.NewDialer(conf.Conf.SMTPHost, conf.Conf.SMTPPort, conf.Conf.SMTPUsername, conf.Conf.SMTPPassword)
if err := dialer.DialAndSend(msg); err != nil {
fmt.Fprintf(os.Stderr, "cannot send email: %s\n", err.Error())
api.Reply(w, 400, api.Answer{})
return
}
api.Reply(w, 200, api.Answer{Success: true})
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

11593
front/assets/css/styles.css Normal file

File diff suppressed because it is too large Load Diff

54
front/assets/css/vars.css Normal file
View File

@ -0,0 +1,54 @@
:root {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #1abc9c;
--bs-cyan: #0dcaf0;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #60768a;
--bs-secondary: #d6974c;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 96, 118, 138;
--bs-secondary-rgb: 214, 151, 76;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg-rgb: 255, 255, 255;
--bs-font-sans-serif: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-bg: #fff;
--bs-body-text-align: "justify";
}

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,74 @@
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Regular.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: local('Roboto'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Italic.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Bold.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 700;
src: local('Roboto'), url('https://fonts.cdnfonts.com/s/12165/Roboto-BoldItalic.woff') format('woff');
}
@font-face {
font-family: 'Roboto Thin';
font-style: normal;
font-weight: 250;
src: local('Roboto Thin'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Thin.woff') format('woff');
}
@font-face {
font-family: 'Roboto Thin';
font-style: italic;
font-weight: 250;
src: local('Roboto Thin'), url('https://fonts.cdnfonts.com/s/12165/Roboto-ThinItalic.woff') format('woff');
}
@font-face {
font-family: 'Roboto Light';
font-style: normal;
font-weight: 300;
src: local('Roboto Light'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Light.woff') format('woff');
}
@font-face {
font-family: 'Roboto Light';
font-style: italic;
font-weight: 300;
src: local('Roboto Light'), url('https://fonts.cdnfonts.com/s/12165/Roboto-LightItalic.woff') format('woff');
}
@font-face {
font-family: 'Roboto Medium';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Medium.woff') format('woff');
}
@font-face {
font-family: 'Roboto Medium';
font-style: italic;
font-weight: 500;
src: local('Roboto Medium'), url('https://fonts.cdnfonts.com/s/12165/Roboto-MediumItalic.woff') format('woff');
}
@font-face {
font-family: 'Roboto Black';
font-style: normal;
font-weight: 900;
src: local('Roboto Black'), url('https://fonts.cdnfonts.com/s/12165/Roboto-Black.woff') format('woff');
}
@font-face {
font-family: 'Roboto Black';
font-style: italic;
font-weight: 900;
src: local('Roboto Black'), url('https://fonts.cdnfonts.com/s/12165/Roboto-BlackItalic.woff') format('woff');
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="40" zoomAndPan="magnify" viewBox="0 0 30 30.000001" height="40" preserveAspectRatio="xMidYMid meet" version="1.0"><defs><clipPath id="id1"><path d="M 2 0 L 20 0 L 20 16 L 2 16 Z M 2 0 " clip-rule="nonzero"/></clipPath><clipPath id="id2"><path d="M 2.363281 0.449219 L 21.980469 9.894531 L 19.464844 15.128906 L -0.15625 5.679688 Z M 2.363281 0.449219 " clip-rule="nonzero"/></clipPath><clipPath id="id3"><path d="M 2.390625 3.925781 L 8.199219 3.925781 L 8.199219 28.601562 L 2.390625 28.601562 Z M 2.390625 3.925781 " clip-rule="nonzero"/></clipPath><clipPath id="id4"><path d="M 10 13 L 28 13 L 28 29 L 10 29 Z M 10 13 " clip-rule="nonzero"/></clipPath><clipPath id="id5"><path d="M 27.625 28.578125 L 8.007812 19.128906 L 10.527344 13.898438 L 30.144531 23.34375 Z M 27.625 28.578125 " clip-rule="nonzero"/></clipPath><clipPath id="id6"><path d="M 21.796875 0.425781 L 27.605469 0.425781 L 27.605469 25.101562 L 21.796875 25.101562 Z M 21.796875 0.425781 " clip-rule="nonzero"/></clipPath></defs><g clip-path="url(#id1)"><g clip-path="url(#id2)"><path fill="#ffffff" d="M 19.433594 8.667969 L 2.371094 0.453125 L 2.390625 6.90625 L 19.457031 15.125 L 19.433594 8.667969 " fill-opacity="1" fill-rule="nonzero"/></g></g><g clip-path="url(#id3)"><path fill="#ffffff" d="M 2.390625 3.933594 L 8.199219 3.933594 L 8.199219 28.59375 L 2.390625 28.59375 L 2.390625 3.933594 " fill-opacity="1" fill-rule="nonzero"/></g><g clip-path="url(#id4)"><g clip-path="url(#id5)"><path fill="#d6974c" d="M 10.554688 20.355469 L 27.617188 28.574219 L 27.597656 22.117188 L 10.535156 13.902344 L 10.554688 20.355469 " fill-opacity="1" fill-rule="nonzero"/></g></g><g clip-path="url(#id6)"><path fill="#d6974c" d="M 27.605469 25.09375 L 21.796875 25.09375 L 21.796875 0.433594 L 27.605469 0.433594 L 27.605469 25.09375 " fill-opacity="1" fill-rule="nonzero"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
front/assets/img/profil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

164
front/assets/js/scripts.js Normal file
View File

@ -0,0 +1,164 @@
/*!
* Start Bootstrap - Freelancer v7.0.5 (https://startbootstrap.com/theme/freelancer)
* Copyright 2013-2021 Start Bootstrap
* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-freelancer/blob/master/LICENSE)
*/
//
// Scripts
//
window.addEventListener('load', _ => {
// Navbar shrink function
let navbarShrink = function () {
const headerCollapsible = document.body.querySelector('#mainHeader');
if (!headerCollapsible) {
return;
}
if (window.scrollY === 0) {
headerCollapsible.classList.remove('shrink');
} else {
headerCollapsible.classList.add('shrink');
}
};
// Display dynamic elements
if (window.scrollY === 0) {
document.body.querySelector(".triangle-left").classList.add("fallen");
document.body.querySelector(".navbar-brand").classList.add("fallen");
window.setTimeout(_ => document.body.querySelector(".triangle-right").classList.add("fallen"), 500);
window.setTimeout(_ => document.body.querySelector(".navbar-toggler").classList.add("fallen"), 500);
window.setTimeout(_ => document.body.querySelector(".navbar-collapse").classList.add("fallen"), 500);
window.setTimeout(_ => document.body.querySelector(".masthead-avatar").classList.add("fallen"), 1000);
window.setTimeout(_ => document.body.querySelector(".masthead-subheading").classList.add("fallen"), 1000);
window.setTimeout(_ => document.body.querySelector(".masthead-heading").classList.add("fallen"), 1500);
} else {
document.body.querySelector(".triangle-left").classList.add("fallen");
document.body.querySelector(".triangle-right").classList.add("fallen");
document.body.querySelector(".navbar-brand").classList.add("fallen");
document.body.querySelector(".navbar-toggler").classList.add("fallen");
document.body.querySelector(".navbar-collapse").classList.add("fallen");
document.body.querySelector(".masthead-avatar").classList.add("fallen");
document.body.querySelector(".masthead-heading").classList.add("fallen");
document.body.querySelector(".masthead-subheading").classList.add("fallen");
}
// Shrink the navbar
navbarShrink();
// Shrink the navbar when page is scrolled
document.addEventListener('scroll', navbarShrink);
// Activate Bootstrap scrollspy on the main nav element
const mainNav = document.body.querySelector('#mainNav');
if (mainNav) {
new bootstrap.ScrollSpy(document.body, {
target: '#mainNav',
offset: 72,
});
}
// Collapse responsive navbar when toggler is visible
const navbarToggler = document.body.querySelector('.navbar-toggler');
const navbarResponsive = document.querySelector("#navbarResponsive");
const responsiveNavItems = [].slice.call(
document.querySelectorAll('#navbarResponsive .nav-link')
);
responsiveNavItems.map(function (responsiveNavItem) {
responsiveNavItem.addEventListener('click', () => {
if (window.getComputedStyle(navbarToggler).display !== 'none') {
navbarToggler.click();
}
});
});
window.addEventListener("click", _ => {
if (navbarResponsive.classList.contains("show")) {
new bootstrap.Collapse(navbarResponsive, true);
}
});
// Set min-height for each grid items
const gridItems = [].slice.call(
document.querySelectorAll(".grid-item")
);
gridItems.map(function(gridItem) {
gridItem.style.minHeight = gridItem.clientHeight + "px";
});
// Handle contact submit
document.getElementById("contactForm").addEventListener("submit", event => {
let form = document.querySelector("#contactForm");
event.preventDefault();
event.stopPropagation();
// Do not submit form if already submitted
if (!$("#submitSuccessMessage").hasClass("d-none")) {
return;
}
if (form.checkValidity()) {
$.ajax({
accepts: {
json: "application/json"
},
contentType: "application/json",
data: JSON.stringify({
name: $("#name").val(),
email: $("#email").val(),
phone: $("#phone").val(),
company: $("#company").val(),
message: $("#message").val(),
captchaId: $("#captchaId").val(),
captchaDigits: $("#captchaDigits").val()
}),
dataType: "json",
method: "POST",
url: "/api/contact/send"
}).done(
_ => {
$("#captchaDigits").removeClass("is-invalid");
$("#submitSuccessMessage").removeClass("d-none");
$("#submitErrorMessage").addClass("d-none");
$("#submitButton").prop("disabled", true);
}
).fail(
xhr => {
if (xhr.status === 418) {
form.classList.remove("was-validated");
$("#captchaDigits").addClass("is-invalid");
generateCaptcha();
} else if (xhr.status === 500) {
$("#captchaDigits").removeClass("is-invalid");
$("#submitSuccessMessage").addClass("d-none");
$("#submitErrorMessage").removeClass("d-none");
}
}
);
}
form.classList.add("was-validated");
});
let generateCaptcha = function() {
$.ajax({
accepts: {
json: "application/json"
},
contentType: "application/json",
dataType: "json",
method: "POST",
url: "/api/captcha/new"
}).done(data => {
$("#captchaImage").attr("src", "/assets/img/captcha/"+data.id+".png");
$("#captchaId").val(data.id);
}).fail(_ => {
$("#captchaImage").attr("src", "");
$("#captchaId").val();
});
}
generateCaptcha();
$("#captchaReload").on("click", generateCaptcha);
$("#year").text(new Date().getFullYear());
});

BIN
front/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

398
front/index.html Normal file
View File

@ -0,0 +1,398 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="" />
<meta name="author" content="" />
<title>Netoïk - Freelance - Expert Cybersécurité - DevOps - DB Admin</title>
<!-- Favicon-->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<!-- Font Awesome icons (free version)-->
<link href="/assets/fontawesome-6.1.1/all.min.css" rel="stylesheet" type="text/css" />
<script src="/assets/fontawesome-6.1.1/all.min.js" type="text/javascript"></script>
<!-- Google fonts-->
<link href="/assets/googleapis/roboto" rel="stylesheet" type="text/css" />
<!-- Core theme CSS (includes Bootstrap)-->
<link href="/assets/css/vars.css" rel="stylesheet" />
<link href="/assets/css/styles.css" rel="stylesheet" />
<!-- JQuery library -->
<script src="/assets/jquery-3.6.0/jquery-3.6.0.min.js"></script>
</head>
<body id="page-top">
<!-- ============================== -->
<!-- Navigation -->
<!-- ============================== -->
<header class="fixed-top text-center" id="mainHeader">
<div class="triangle triangle-left"></div>
<div class="triangle triangle-right"></div>
<nav class="navbar navbar-expand-lg" id="mainNav">
<div class="container">
<a class="navbar-brand" href="#page-top">
<img src="/assets/img/netoik-favicon-white.svg" alt="N">
</a>
<button class="navbar-toggler text-uppercase font-weight-bold border-white text-white rounded" type="button"
data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive"
aria-expanded="false" aria-label="Toggle navigation">
<span class="d-none d-sm-inline">Menu</span>
<i class="fas fa-bars"></i>
</button>
<div class="collapse navbar-collapse text-uppercase" id="navbarResponsive">
<ul class="navbar-nav ms-auto">
<li class="nav-item mx-0 mx-lg-1">
<a class="nav-link nav-link-services py-3 px-0 px-lg-3 rounded" href="#services">Prestations</a>
</li>
<li class="nav-item mx-0 mx-lg-1">
<a class="nav-link nav-link-about py-3 px-0 px-lg-3 rounded" href="#about">A propos</a>
</li>
<li class="nav-item mx-0 mx-lg-1">
<a class="nav-link nav-link-contact py-3 px-0 px-lg-3 rounded" href="#contact">Contact</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<!-- ============================== -->
<!-- Masthead -->
<!-- ============================== -->
<header class="masthead text-primary text-center">
<div class="container d-flex align-items-center flex-column">
<!-- Masthead Avatar Image-->
<img class="masthead-avatar" src="/assets/img/profil.png" alt="..." />
<!-- Masthead Heading-->
<h1 class="masthead-heading">
<img src="/assets/img/netoik-title.svg" alt="Netoïk">
</h1>
<div class="masthead-subheading font-weight-light mb-0">
<!-- Icon Divider-->
<div class="divider-custom">
<div class="divider-custom-line bg-primary border-primary"></div>
<div class="divider-custom-icon text-primary"><i class="fas fa-star"></i></div>
<div class="divider-custom-line bg-primary border-primary"></div>
</div>
<!-- Masthead Subheading-->
<h2 class="mb-4">Samuel Campos</h2>
<div class="mx-2">cybersecurity expert &middot; devOps freelance</div>
</div>
</div>
</header>
<!-- ============================== -->
<!-- Services -->
<!-- ============================== -->
<section class="page-section" id="services">
<div class="container">
<!-- Services Section Heading-->
<h2 class="page-section-heading text-center text-primary text-uppercase mb-0">Prestations</h2>
<!-- Icon Divider-->
<div class="divider-custom">
<div class="divider-custom-line bg-primary border-primary"></div>
<div class="divider-custom-icon text-primary"><i class="fas fa-star"></i></div>
<div class="divider-custom-line bg-primary border-primary"></div>
</div>
<!-- Service items-->
<div class="row">
<div class="col-lg-8 col-xl-7 m-auto">
<!-- Service Cybersecurity-->
<div class="row py-5 mx-auto">
<div class="col-md-6 text-center d-flex align-self-center py-3">
<img class="img-fluid my-auto" src="/assets/img/services/cybersecurity.svg" alt="cybersecurity">
</div>
<div class="col-md-6 text-primary d-flex align-self-center py-3">
<span>
La <strong>cybersécurité</strong> est primordiale dans un projet <strong>devOps</strong>,
elle passe par le respect des principes de développement <strong>secure-by-design</strong>:
<ul>
<li>
Identifier les menaces
</li>
<li>
Minimiser la surface d'attaque
</li>
<li>
Séparer et restreindre les privilèges
</li>
<li>
Garder les erreurs internes confidentielles
</li>
<li>
Se méfier des services utilisés
</li>
<li>
Eviter la sécurité par l'obscurité
</li>
<li>
Fixer rapidement et correctement les problèmes de sécurité
</li>
</ul>
</span>
</div>
</div>
<!-- Service Programming-->
<div class="row py-5 mx-auto">
<div class="col-md-6 order-md-last d-flex align-self-center py-3">
<img class="img-fluid" src="/assets/img/services/programming.svg" alt="programming">
</div>
<div class="col-md-6 d-flex align-self-center text-primary py-3">
<span>
Selon les contraintes du projet, différents outils et langages de
<strong>programmation</strong> peuvent être utilisés:
<ul>
<li>
Un <strong>langage haut-niveau</strong> POO (Python, PHP) pour un développement rapide
</li>
<li>
Un <strong>langage bas-niveau</strong> (Go, C) pour de meilleures performance
</li>
<li>
Des langages et <strong>frameworks</strong> web (HTML, JS, JQuery, VueJS, CSS, Bootstrap)
</li>
<li>
Des <strong>serveurs</strong> ou <strong>proxys</strong> (Apache, Nginx, HAProxy)
</li>
</ul>
</span>
</div>
</div>
<!-- Service Database-->
<div class="row py-5 mx-auto">
<div class="col-md-6 d-flex align-self-center py-3">
<img class="img-fluid" src="/assets/img/services/database.svg" alt="database">
</div>
<div class="col-md-6 d-flex align-self-center text-primary py-3">
<span>
Chaque <strong>système de gestion de bases de données</strong> (SGBD)
possède ses avantages et ses inconvénients:
<ul>
<li>
Les <strong>BDD relationnelles</strong> (PostgreSQL, MariaDB/MySQL, SQLite)
sont souvent suffisantes pour couvrir la plupart des besoins.
</li>
<li>
Les <strong>BDD non relationnelles</strong> (Redis, MongoDB, Cassandra/Scylla)
sont parfois nécessaires pour des besoins spécifiques.
</li>
</ul>
</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================== -->
<!-- About -->
<!-- ============================== -->
<section class="page-section bg-primary text-white mb-0" id="about">
<div class="container">
<!-- About Section Heading-->
<h2 class="page-section-heading text-center text-uppercase text-white">A propos</h2>
<!-- Icon Divider-->
<div class="divider-custom divider-light">
<div class="divider-custom-line"></div>
<div class="divider-custom-icon"><i class="fas fa-star"></i></div>
<div class="divider-custom-line"></div>
</div>
<!-- About Section Content-->
<div class="row">
<div class="col-lg-8 col-xl-7 m-auto py-5">
<p>
Après avoir obtenu un master de <strong>cryptologie</strong> au sein du plus ancien
master spécialisé de France:
<a class="text-secondary fw-bold text-decoration-none" href="https://www.cryptis.fr" target="_blank">Cryptis</a>,
j'ai décidé de me tourner vers la cybersécurité pour ma vie professionnelle.
</p>
<p>
Mon objectif principal a toujours été d'aider les entreprises à protéger leurs
<strong>données</strong> et surtout celles de leurs clients.
</p>
<p>
Pendant 5 années chez le leader européen de la <strong>cybersécurité offensive</strong>
<a class="text-secondary fw-bold text-decoration-none" target="_blank" href="https://www.ziwit.com">Ziwit</a>,
j'ai participé au développement d'un produit permettant de tester et détecter
tout type de faille de sécurité sur un site web, ainsi que toute une gamme d'outils
pour aider les entreprises à surveiller leur <strong>SI</strong>.
</p>
<p>
Puis une année passée au sein de la société Ozon, spécialiste de la cybersécurité pour les PME,
m'a permis de développer mes compétences sur plus de technologies de développement, déploiement,
surveillance et réseaux.
</p>
<p>
Ces expériences m'ont permis de réaliser qu'en cybersécurité, le risque zéro n'existe pas... Le
meilleur moyen de minimiser ce risque est de concevoir les projets en respectant les principes
de l'approche <strong>secure-by-design</strong>.
</p>
<p>
Ma volonté est donc de réaliser des projets <strong>bien documentés</strong>,
<strong>fiables</strong>, <strong>pérennes</strong> et respectant les principes précités.
</p>
</div>
</div>
<div class="text-center mt-4">
<!-- CV-->
<a class="btn btn-xl btn-outline-light m-3" href="/assets/docs/cv-2022.pdf" target="_blank">
<i class="fas fa-download me-2"></i>
Consulter mon CV
</a>
<!-- Master-->
<a class="btn btn-xl btn-outline-light m-3" href="/assets/docs/master-cryptis.pdf" target="_blank">
<i class="fas fa-download me-2"></i>
Voir mon diplôme
</a>
</div>
</div>
</section>
<!-- ============================== -->
<!-- Contact -->
<!-- ============================== -->
<section class="page-section" id="contact">
<div class="container">
<!-- Contact Section Heading-->
<h2 class="page-section-heading text-center text-uppercase text-primary mb-0">Contact</h2>
<!-- Icon Divider-->
<div class="divider-custom">
<div class="divider-custom-line bg-primary border-primary"></div>
<div class="divider-custom-icon text-primary"><i class="fas fa-star"></i></div>
<div class="divider-custom-line bg-primary border-primary"></div>
</div>
<!-- Contact Section Form-->
<div class="row justify-content-center">
<div class="col-lg-8 col-xl-7 py-5">
<form id="contactForm" class="needs-validation" novalidate>
<!-- Name input-->
<div class="form-floating mb-3">
<input class="form-control" id="name" type="text" required/>
<label for="name">Nom complet *</label>
<div class="invalid-feedback">Merci de saisir un nom.</div>
</div>
<!-- Email address input-->
<div class="form-floating mb-3">
<input class="form-control" id="email" type="email" required/>
<label for="email">Adresse email *</label>
<div class="invalid-feedback">Merci de saisir une adresse email valide.</div>
</div>
<!-- Phone number input-->
<div class="form-floating mb-3">
<input class="form-control" id="phone" type="tel"/>
<label for="phone">Téléphone</label>
</div>
<!-- Company input-->
<div class="form-floating mb-3">
<input class="form-control" id="company" type="text"/>
<label for="company">Company</label>
</div>
<!-- Message input-->
<div class="form-floating mb-3">
<textarea class="form-control" id="message" type="text" style="height: 10rem" required></textarea>
<label for="message">Message *</label>
<div class="invalid-feedback">Merci de saisir un message.</div>
</div>
<!-- Captcha image-->
<div>
<span class="d-inline-block">
<img src="" alt="captcha error" id="captchaImage">
</span>
<span id="captchaReload" role="button">
<i class="fas fa-rotate-right"></i>
</span>
<input type="hidden" id="captchaId">
</div>
<!-- Captcha input-->
<div class="form-floating mb-3">
<input class="form-control" id="captchaDigits" type="text" required>
<label for="captchaDigits">Captcha *</label>
<div class="invalid-feedback">Merci de saisir les chiffres du captcha.</div>
</div>
<!-- Submit Button-->
<button class="btn btn-primary btn-xl" id="submitButton" type="submit">Envoyer</button>
<div id="submitSuccessMessage" class="d-none text-success fw-bold py-2">
Votre message a bien été envoyé!<br/>
Vous recevrez une réponse dans les plus bref délais.
</div>
<div id="submitErrorMessage" class="d-none text-danger fw-bold py-2">
Une erreur est survenue!<br/>
Si cette erreur persiste, vous pouvez m'envoyer un email à
<a href="mailto:samuel.campos@netoik.io">samuel.campos@netoik.io</a>.
</div>
</form>
</div>
</div>
</div>
</section>
<!-- ============================== -->
<!-- Footer -->
<!-- ============================== -->
<footer class="footer text-center bg-secondary">
<div class="container">
<div class="row">
<!-- Footer Location-->
<div class="col-lg-6 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">Adresse</h4>
<p class="lead mb-0">
6 avenue Arletty
<br />
Bois d'Arcy 78390
</p>
</div>
<!-- Footer Social Icons-->
<div class="col-lg-6 mb-5 mb-lg-0">
<h4 class="text-uppercase mb-4">Sur le web</h4>
<a class="btn btn-outline-light btn-social mx-1" target="_blank"
href="https://www.linkedin.com/in/samuel-yann-campos-8b9343b7">
<i class="fab fa-fw fa-linkedin-in"></i>
</a>
<a class="btn btn-outline-light btn-social mx-1" target="_blank" href="https://git.netoik.io">
<i class="fas fa-fw fa-code-compare"></i>
</a>
<a class="btn btn-outline-light btn-social mx-1" target="_blank"
href="https://www.codingame.com/profile/dae0a56ab4422412b9d9fb52fb2580250211771">
<img class="img-light" src="/assets/img/codingame-white.png" alt="codingame">
<img class="img-dark" src="/assets/img/codingame-black.png" alt="codingame">
</a>
</div>
</div>
</div>
</footer>
<!-- ============================== -->
<!-- Copyright -->
<!-- ============================== -->
<div class="copyright py-4 bg-primary text-center text-white">
<div class="container"><small>Copyright &copy; Netoïk <span id="year"></span></small></div>
</div>
<!-- ============================== -->
<!-- Bootstrap core js -->
<!-- ============================== -->
<script src="/assets/bootstrap-5.1.3/bootstrap.bundle.min.js"></script>
<!-- ============================== -->
<!-- Core theme js -->
<!-- ============================== -->
<script src="/assets/js/scripts.js"></script>
</body>
</html>

110
front/services.html Normal file
View File

@ -0,0 +1,110 @@
<!-- ============================== -->
<!-- Services -->
<!-- ============================== -->
<section class="page-section" id="services">
<div class="container">
<!-- Services Section Heading-->
<h2 class="page-section-heading text-center text-primary text-uppercase mb-0">Prestations</h2>
<!-- Icon Divider-->
<div class="divider-custom">
<div class="divider-custom-line bg-primary border-primary"></div>
<div class="divider-custom-icon text-primary"><i class="fas fa-star"></i></div>
<div class="divider-custom-line bg-primary border-primary"></div>
</div>
<!-- Services Grid Items-->
<div class="row justify-content-center">
<!-- Services Item Security-->
<div class="col-md-6 col-lg-4 mb-5 mt-3">
<div class="grid-item bg-primary mx-auto" data-bs-toggle="modal" data-bs-target="#servicesModalSecurity">
<div class="caption bg-primary">
<div class="col-8 mx-auto">
<img class="img-fluid my-4" src="/assets/img/services/shield.png" alt="..."/>
</div>
</div>
<div class="text bg-primary text-white p-4">
La cybersécurité est primordiale dans un projet devOps, elle passe par le
respect des principes de développement secure-by-design:
<ul>
<li>
Identifier les menaces
</li>
<li>
Minimiser la surface d'attaque
</li>
<li>
Séparer et restreindre les privilèges
</li>
<li>
Garder les erreurs internes confidentielles
</li>
<li>
Ne pas faire confiance aux services
</li>
<li>
Eviter la sécurité par l'obscurité
</li>
<li>
Fixer rapidement et correctement les problèmes de sécurité
</li>
</ul>
</div>
</div>
</div>
<!-- Services Item Coding-->
<div class="col-md-6 col-lg-4 mb-5 mt-3">
<div class="grid-item bg-primary mx-auto" data-bs-toggle="modal" data-bs-target="#servicesModalCoding">
<div class="caption bg-primary">
<div class="col-8 mx-auto">
<img class="img-fluid my-4" src="/assets/img/services/programming.png" alt="..."/>
</div>
</div>
<div class="text bg-primary text-white p-4">
Le choix des technologies dépend des contraintes du projet:
<ul>
<li>
langage haut-niveau POO (Python, PHP)
</li>
<li>
langage bas-niveau (Go, C)
</li>
<li>
les langages web (HTML, JS, JQuery, VueJS, CSS, Bootstrap)
</li>
<li>
les serveurs et/ou proxys (Apache, Nginx, HAProxy)
</li>
</ul>
</div>
</div>
</div>
<!-- Services Item Database-->
<div class="col-md-6 col-lg-4 mb-5 mt-3">
<div class="grid-item bg-primary mx-auto" data-bs-toggle="modal" data-bs-target="#servicesModalDatabase">
<div class="caption bg-primary">
<div class="col-8 mx-auto">
<img class="img-fluid my-4" src="/assets/img/services/database.png" alt="..."/>
</div>
</div>
<div class="text bg-primary text-white p-4">
Le choix du SGBD dépend aussi du projet:
<ul>
<li>
BDD relationnelles (PostgreSQL, MariaDB/MySQL, SQLite)
</li>
<li>
BDD non relationnelles (Redis, MongoDB, Cassandra/Scylla)
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>