first commit
|
@ -0,0 +1,2 @@
|
|||
.idea
|
||||
*.conf
|
|
@ -0,0 +1,2 @@
|
|||
build:
|
||||
go build -o bin/server cmd/server/main.go
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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})
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 3.5 KiB |
|
@ -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 |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 7.8 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 45 KiB |
|
@ -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());
|
||||
|
||||
});
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -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 · 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 © 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>
|
|
@ -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>
|