Practical Web Based Deep Learning and Security by Example

Daisy's profile photo

By Daisy

Practical Web Based Deep Learning and Security by Example Third Edition Charlotte Harper July 3, 2024 Foreword: Security considerations in building software for the web are an important part of any web developer's plan and execution while engineering a prototype that is dependable, stable, and useful for practical purposes. The DOM (Document Object Markup), with it's implementation of HTML, JavaScript, and CSS as well as backend software implementing Python, C/C++, Java and bash, give web developers the freedom and power to create a wide variety of projects that express creativity, provide ease of use and functionality, portray humility and character, and provide ease of use as well as convenience and important services that are all attractive to the average Joe, the end user looking to kill time or get something done on the internet, usually on a touchscreen smartphone device. Most people wouldn't even know where to start when they want to build a website from scratch, they would tend to start on another person's website and build something limited in functionality, dependability, ease of use and especially creativity when they could have had all the latest powerful tools at their disposal in order to build something useful without wasting time pressing buttons, and especially wasting money paying for expensive subscriptions to software few people wanted to use anyway given it's limitations in ease of use and flexibility. If you have a few minutes to read through this book and learn what I want to teach you, or even speak with me personally about your goals and get some guidance in the right direction, and are motivated to learn to code and write your own software, take this book home and set aside some time to learn to build the next influential, powerful, streamlined and important web application, a website that is all on you and does exactly what you want and meets the needs of your audience. About me: I am a software developer with a wide range of experience in C/C++, Java, Python, HTML, CSS and JavaScript. I build websites people want to use, want to visit, and even get addicted to using just to learn, recreate and kill time, and most importantly, I sell software. If you had an idea as to exactly how you wanted a website to look and function, you were willing to support me so I can meet my own needs while I meet yours, and you are willing to cover the costs of running a website yourself, I would build you the next YouTube, TikTok, Twitter, Google, or even a high-tech security app only you can access. Instead of trying to sell you my time, I'm trying to buy yours: I want to talk you into building an app (website) yourself with the information that already exists, and teach you what you need to be an independent software developer, entrepreneur, leading a successful career in whatever field you desire. And let me be clear, the education I give you will be informal. You could go to school and learn all of this with a formal education, or even read this book in school, complete your assignments, and take away a great deal from your education, but I won't formally put you in the hot seat and ask you to complete assignments. I'm not your professor, you can think of me like a friend who wants to guide you toward a career driven by your own personal success. And I'm not selling you success either, you will need to buy it with your time. Learning to code has a steep learning curve and was never easy, or even supposed to be. You need to work as hard as you possibly can and continue to try and fail and try again even when you are frustrated in order to learn and build apps yourself. That's in the nature of code itself. Code is run by a compiler that is designed to give the programmer error messages, and these will teach you how to code, even if you are simply copying the error into your search engine and reading other people's examples. And I must say, you don't need to be extremely rich, smart, successful, or even detail oriented or organized to build an app. The computer takes care of that organization for you. You just need to persevere through the trial and error, maintain focus and work hard at what you do, and you will have a very successful career in the entirety of what you do. Who I am: I realize that the last section was more about learning and your take a ways from this book. Who am I exactly? That's a complicated question. I am unclear on that myself, as I suffer from medical conditions that can make it difficult for me to even code or write this book at times, while presenting challenges with socialization and identity issues that make my life more difficult when it comes to introducing myself. In short, if you are reading this book, you brought it home because you flipped through it and thought it was useful, or even if you just read this far in, to you I am a like minded individual who wants to see you succeed in everything that you do. I am an engineer myself, a software developer, and a student, and I am writing this book for other students who want to make their lives easier by having a handbook of the software they need making their lives easier by giving examples to copy that fit together like a big puzzle into a working, useful, large, functional, cohesive, and engaging app that can drive success no matter the line of business. Largely, this is what I do: I build apps to help myself and other people succeed. I am an author as well, though this is my first publication that I intend to complete in order to put my portfolio together into a useful document, and I am an artist as well. I'll admit this to you, I'm sort of a strange person. I'm not perfect, I've had run ins with the law even leading me to leave colleges and universities and leave states in order to try to make a name for myself with more success. I am a woman by birth, I wear makeup, take photos of myself, wear dresses and other womens clothing, and I stay conscious of myself as a female by nature. I've had issues with other people in the past that lead to struggles with writing and building webapps, and I apologize that I haven't been able to get this book in your hands sooner: You needed this. You will want to read and write code that looks like mine and works like mine and does the same thing but even better, because if you can afford to buy this book instead of mashing your keyboard like I do just to create a book yourself asking money for it, you have the resources you need to be successful in your life. I had all sorts of issues with family growing up, health conditions, doctors, the media, and the law, and my code deeply reflects the struggle that is feminism and female nature in a divided and frustrated world. However, this book is something I deeply care about, my baby, my portfolio, and my livelihood, so I appreciate your consideration when you take the text home and carefully pore over it in order to learn from me. Please keep in mind I am not perfect, this book will have errors, revisions, and new editions, and you will need to think with your logical brain as best you can in order to have a successful experience with my writing. Also, understand that I mean well for you even when you face challenges when writing. Think about it like this: When you can just rent a computer system to do anything you can possibly imagine in the digital space, store all the information you encounter, #$%!yze and organize it, and come to understand it, you will inevitably encounter difficulties with the information you are ingesting and even publishing. I tell you this because I encounter the same difficulties. Use this book at your own risk, work with your community and communities available to you to build software within a safe setting, and don't take things to personally when you fail or even succeed in the wrong way: That's how I got this far, and why I can bring you this text and help you succeed without diverging off on a path of madness that leaves me ruined, torn and frayed while I encounter the ordinary problems everyone does on a global scale thanks to the paralellistic global scale of the network on which we will work, the internet. You might not be very familiar with who I am with just a few words, but I encourage you to read on, you will get to know me as you continue to read and understand me while building your own projects to complete your work. There will be no homework with this book, as long as your professors or teachers don't assign you any, but I highly encourage you to build a portfolio of projects yourself as you read along, as well as a capstone project showcasing how you can apply what you have learned. My capstone project is the basis for most of what you will read in this book, as it incorporates code from my previous projects, code I have created and learned to write methodically by hand, and a wide range of ideas and tips that have helped me succeed to the point where I can spin up a simple app that is fully featured and looks and behaves like a popular app you might see your friend or family using, on the internet, advertised to you, or in the news. What this book is: This book is a tutorial by example. You can find code here, instructions for how to learn to code, information on debugging code and fixing errors, troubleshooting steps, instructions on how to back up and save your code, re-deploy if anyone breaks your code, secure your code, deploy your code, build interactive websites that are entertaining, engaging, and addictive, and you will get a sense of who I am, why this is important, and how to portray yourself, your app and company image, as well as the software you build in the absolute best light to be the most attractive as possible to your end users, your website's visitors. In this book, I will demonstrate a number of examples of software design with a focus on the web as a platform as well as security. We will initiate the learning experience by building a basic project...
Practical Web Based Deep Learning and Security by Example

Practical Web Based Deep Learning and Security by Example Third Edition Charlotte Harper July 3, 2024 Foreword: Security considerations in building software for the web are an important part of any web developer's plan and execution while engineering a prototype that is dependable, stable, and useful for practical purposes. The DOM (Document Object Markup), with it's implementation of HTML, JavaScript, and CSS as well as backend software implementing Python, C/C++, Java and bash, give web developers the freedom and power to create a wide variety of projects that express creativity, provide ease of use and functionality, portray humility and character, and provide ease of use as well as convenience and important services that are all attractive to the average Joe, the end user looking to kill time or get something done on the internet, usually on a touchscreen smartphone device. Most people wouldn't even know where to start when they want to build a website from scratch, they would tend to start on another person's website and build something limited in functionality, dependability, ease of use and especially creativity when they could have had all the latest powerful tools at their disposal in order to build something useful without wasting time pressing buttons, and especially wasting money paying for expensive subscriptions to software few people wanted to use anyway given it's limitations in ease of use and flexibility. If you have a few minutes to read through this book and learn what I want to teach you, or even speak with me personally about your goals and get some guidance in the right direction, and are motivated to learn to code and write your own software, take this book home and set aside some time to learn to build the next influential, powerful, streamlined and important web application, a website that is all on you and does exactly what you want and meets the needs of your audience. About me: I am a software developer with a wide range of experience in C/C++, Java, Python, HTML, CSS and JavaScript. I build websites people want to use, want to visit, and even get addicted to using just to learn, recreate and kill time, and most importantly, I sell software. If you had an idea as to exactly how you wanted a website to look and function, you were willing to support me so I can meet my own needs while I meet yours, and you are willing to cover the costs of running a website yourself, I would build you the next YouTube, TikTok, Twitter, Google, or even a high-tech security app only you can access. Instead of trying to sell you my time, I'm trying to buy yours: I want to talk you into building an app (website) yourself with the information that already exists, and teach you what you need to be an independent software developer, entrepreneur, leading a successful career in whatever field you desire. And let me be clear, the education I give you will be informal. You could go to school and learn all of this with a formal education, or even read this book in school, complete your assignments, and take away a great deal from your education, but I won't formally put you in the hot seat and ask you to complete assignments. I'm not your professor, you can think of me like a friend who wants to guide you toward a career driven by your own personal success. And I'm not selling you success either, you will need to buy it with your time. Learning to code has a steep learning curve and was never easy, or even supposed to be. You need to work as hard as you possibly can and continue to try and fail and try again even when you are frustrated in order to learn and build apps yourself. That's in the nature of code itself. Code is run by a compiler that is designed to give the programmer error messages, and these will teach you how to code, even if you are simply copying the error into your search engine and reading other people's examples. And I must say, you don't need to be extremely rich, smart, successful, or even detail oriented or organized to build an app. The computer takes care of that organization for you. You just need to persevere through the trial and error, maintain focus and work hard at what you do, and you will have a very successful career in the entirety of what you do. Who I am: I realize that the last section was more about learning and your take a ways from this book. Who am I exactly? That's a complicated question. I am unclear on that myself, as I suffer from medical conditions that can make it difficult for me to even code or write this book at times, while presenting challenges with socialization and identity issues that make my life more difficult when it comes to introducing myself. In short, if you are reading this book, you brought it home because you flipped through it and thought it was useful, or even if you just read this far in, to you I am a like minded individual who wants to see you succeed in everything that you do. I am an engineer myself, a software developer, and a student, and I am writing this book for other students who want to make their lives easier by having a handbook of the software they need making their lives easier by giving examples to copy that fit together like a big puzzle into a working, useful, large, functional, cohesive, and engaging app that can drive success no matter the line of business. Largely, this is what I do: I build apps to help myself and other people succeed. I am an author as well, though this is my first publication that I intend to complete in order to put my portfolio together into a useful document, and I am an artist as well. I'll admit this to you, I'm sort of a strange person. I'm not perfect, I've had run ins with the law even leading me to leave colleges and universities and leave states in order to try to make a name for myself with more success. I am a woman by birth, I wear makeup, take photos of myself, wear dresses and other womens clothing, and I stay conscious of myself as a female by nature. I've had issues with other people in the past that lead to struggles with writing and building webapps, and I apologize that I haven't been able to get this book in your hands sooner: You needed this. You will want to read and write code that looks like mine and works like mine and does the same thing but even better, because if you can afford to buy this book instead of mashing your keyboard like I do just to create a book yourself asking money for it, you have the resources you need to be successful in your life. I had all sorts of issues with family growing up, health conditions, doctors, the media, and the law, and my code deeply reflects the struggle that is feminism and female nature in a divided and frustrated world. However, this book is something I deeply care about, my baby, my portfolio, and my livelihood, so I appreciate your consideration when you take the text home and carefully pore over it in order to learn from me. Please keep in mind I am not perfect, this book will have errors, revisions, and new editions, and you will need to think with your logical brain as best you can in order to have a successful experience with my writing. Also, understand that I mean well for you even when you face challenges when writing. Think about it like this: When you can just rent a computer system to do anything you can possibly imagine in the digital space, store all the information you encounter, #$%!yze and organize it, and come to understand it, you will inevitably encounter difficulties with the information you are ingesting and even publishing. I tell you this because I encounter the same difficulties. Use this book at your own risk, work with your community and communities available to you to build software within a safe setting, and don't take things to personally when you fail or even succeed in the wrong way: That's how I got this far, and why I can bring you this text and help you succeed without diverging off on a path of madness that leaves me ruined, torn and frayed while I encounter the ordinary problems everyone does on a global scale thanks to the paralellistic global scale of the network on which we will work, the internet. You might not be very familiar with who I am with just a few words, but I encourage you to read on, you will get to know me as you continue to read and understand me while building your own projects to complete your work. There will be no homework with this book, as long as your professors or teachers don't assign you any, but I highly encourage you to build a portfolio of projects yourself as you read along, as well as a capstone project showcasing how you can apply what you have learned. My capstone project is the basis for most of what you will read in this book, as it incorporates code from my previous projects, code I have created and learned to write methodically by hand, and a wide range of ideas and tips that have helped me succeed to the point where I can spin up a simple app that is fully featured and looks and behaves like a popular app you might see your friend or family using, on the internet, advertised to you, or in the news. What this book is: This book is a tutorial by example. You can find code here, instructions for how to learn to code, information on debugging code and fixing errors, troubleshooting steps, instructions on how to back up and save your code, re-deploy if anyone breaks your code, secure your code, deploy your code, build interactive websites that are entertaining, engaging, and addictive, and you will get a sense of who I am, why this is important, and how to portray yourself, your app and company image, as well as the software you build in the absolute best light to be the most attractive as possible to your end users, your website's visitors. In this book, I will demonstrate a number of examples of software design with a focus on the web as a platform as well as security. We will initiate the learning experience by building a basic project using the UNIX shell, with backup and scripting features. Then, we will examine a basic blog website, upgrade our blog with photo and video features as well as use these features to employ security solutions using free software, and secure our server using a pluggable authentication module (PAM). We will then review file handling and processing, exploring video editing, voice donation, barcode scanning and optical character recognition, among other concepts. Along the way we will examine APIs which will help us make our software more useful and secure, with free and paid options. Along the way, we will explore physical security and militant tools such as firearms and munitions design and manufacturing including barrel and repeater design, turret and drone design, and other principals we will integrate with our software on the existing network in order to protect our software and demonstrate self defense and resillience. We will take breaks along the way to build games, 2D and 3D rendering engines, and work with embedded hardware in case study examples of basic dimensional rendering software and an electronic vibrating massager cast in silicone rubber respectively. Along the way, we will also employ machine learning solutions already available in order to better secure our software. We will also employ stock tools available for the web in order to streamline and secure the process. This book is a guide to your success in building a web application and integrating it with a professional network of computer and embedded mechanical systems, and overall a guide to building software and embedded hardware with no background knowledge or previous experience. What this book isn't: If you really want to have a website, you could just set up a simple store and sell what you need, post a blog, post photos or videos, or otherwise without ever writing a single line of code. This book isn't that. This book will teach you how to build software that is more useful, fully featured, functional and secure than any software you can already find, because it deploys the latest software that is still prototypes, may be expensive to run at a scale older companies operate at, and doesn't appeal to backwards, convoluted companies set up to make money for people who aren't really doing anything. If you follow this book closely, you will want to write code, research code, build your own apps, and you will make money from what you do. I will make money from this book, even in early stages, because it contains information people need and want to read, and are already buying when they buy or use my apps. This book won't build an app for you, but it will point you in the right direction and arm you with the tools you need and the skills and tips that will facilitate your own success in building software for the web, with every line of code you will need to write as an example, ready to be pieced together into software you and your supporters, guests, clientele, friends, family, visitors, contractors, and the people of the internet want to use and support. What you will learn: This book will teach you how to build and sell software, really functional, useful software, media recording, security features like facial recognition, machine readable zone barcode scanning, web APIs to authenticate, record and render video and photos, and exchange messages like bluetooth and near field (NFC) communication. This book will teach you how to use a networked computer, focusing on debian linux, how to build bash code to make installing and backing up your software a seamless, automated breeze, how to build python code as a backend to serve dynamic messages, style things nicely using CSS styles with Bootstrap, enable user logins and interactivity through networked devices, build interactive media and network with other websites to offer security features like text messages for verification or other purposes, ID scanning, image and video moderation, data microtransactions to keep your software secure, payment processing, cryptocurrency trading, asynchronous tasks, and more. You will learn how to build your own bluetooth devices, with batteries, chargers, microcontrollers, circuits, motors and sensors, using solder, wire and 3D printed as well as cast materials. I will demonstrate 3D design principals applied to additive manufacturing and tool and die making, so you are able to manufacture your own embedded, hardware devices with integrated batteries, chargers, electronic circuits, and functional outputs. and network them with bluetooth and the web. Specifically, we will examine two case studies, a vibrating massager and a homemade firearm, both programmed in OpenSCAD, which is available as a graphical interface or command line utility and can be integrated into a web for speedier results. You will learn how to build and deploy a website from the ground up with no prior experience, make it functional, secure, beautiful, useful and most importantly practical. You will learn how to use machine learning and computer vision to make a site secure and more practical, record video and audio from your website, donate your voice, make music and modulate audio to create useful samples, and how to break through the noise by leveraging other websites to build the best possible network of websites that you can link directly to yours in order to share all the useful information you have to offer, and even more importantly bring people to your software and business. This book will be focused most heavily on media, security and machine learning, which are the major three components that will help you build useful software for the web by engaging the right users and disengaging the wrong ones in a manner that is realistic, practical, hands on and engaging while also automatic, and sturdy. This book teaches UNIX, specifically Debian (Ubuntu), Bash shell, Python, HTML, CSS, JavaScript, and a number of useful software packages for Python like requests, as well as useful bash software like git and ffmpeg. I will also teach you how to trade cryptocurrency automatically, and take payments in cryptocurrency or from regular debit cards while even paying out your visitors a share of your revenue if you choose to do so. I will teach you how to make money from your website through advertising as well, how to ready your app for search engines and make it fast, ranked in the first ranking for what your customers will search to find you, and ranking in as many common searches as possible. I will teach you how to sell your software, advertise it, appeal to clients looking for your services, and make a name for yourself on the internet through avenues that already exist, are inexpensive, and work well. I will teach you how to save your data on cloud computers that work for you and save your data cheaply, how to plan and build a website that does what your users want and what you want, and how to keep your users engaged by putting your site a tap away on their phones with notifications, email, text messages, phone calls, and more avenues to bring your users back to your website at your disposal behind the click of a button secured to you only. This book will focus on the practicality of publishing and distributing media in large amounts, from text to photos to videos to audio, making a good impression on end users (your clientele), and selling yourself in any way that you do in order to create a website, an app that is representative of you and you only, and makes you, your software and your company look good in the best way possible. You will also learn a few tips and tricks from me, from coding tips, practical vanity like makeup and photography, modeling and acting, and more, which will be important for portraying yourself and your company in the best possible light using all the tools available to you while distributing as much content as you need across a healthy balance of platforms to bring your software to fruition with no more effort, work, or money than is necessary. This book is called “Practical Web Based Deep Learning and Security by Example” for a reason: It deals with learning to code, specifically for the web, specifically with a focus on security, from a practical standpoint, with examples of working code that serves the practical purposes outlined in the text. The learning component of this text also encompasses machine learning, the code I will show you how to run for the web that will handle computer vision, facial recognition, image and video moderation, image enhancement, resolution enhancement, image captioning, and other tasks like prediction metrics sourced from images, such as the nature of the image as an authentic, computer-transferred image, or an optical copy (a photo of an image, or printed photo). Machine learning is very important when it comes to web security and software security, because it can preform tasks that were otherwise impossible. Your computer might log you in with a passcode, but it may be safer to use it if it logs you in with your face. You can make a server computer this safe, a computer that would normally ask you for a username and passcode and log you in, maybe with a confirmation token for each new login or new IP address, but if you are building large scale, easy to use, fundamentally secure, and powerful software, this may be enough. Tying your software too closely to someone else's software, like an email service or text message service, is not enough to make your software secure, or anyone's (any site you use). Anyone who builds software that is impeccably secure has some sense of what this implies. Software is inherently insecure because the devices and accounts we use to access it are not always at our disposal, they could be in the hands of anyone with ill intent for the software and therefore may pose a risk to the software itself. This is something of the focus of this book. A networked computer is by default secured with a long key token, called and SSH or Secure Shell key, and is otherwise best secured with a web server, because the web server provides the open access as well as state of the art security tools running on the server itself. The web server has access to the user's web browser, which is arguably the most powerful part of the user's device, because it's the place where the user can access networked software. This toolkit can render text, the webpages you see, and can also record images, audio and video (like a photo of a face or a state ID), can read and write to Bluetooth radio devices, and can read and write to near field transponder tags, inexpensive key cards, fobs, stickers, rings and even chip implants with unique serial numbers that can be read and written to with data generated and validated by a web server tied to the web site. Using all of the tools at your disposal, with this book you will equip yourself with the knowledge to build a secure website, and overall a secure networked computer system that works for you, does your bidding, and looks and feels right. Where to start: You're welcome to skip past the section I begin this book with, or any section, to the exact code you need, especially if you have experience with coding before or any of the aforementioned tools I will be describing in detail in this book as well as documenting use cases and practical examples thereof. If you don't have experience in writing code, I highly recommend you read all of this book, and especially recommend you read the previous sections, to make sure this book is right for you. If this book isn't right for you, consider gifting it to a friend or relative who might be interested in learning about web development themselves, and even consider borrowing it back and learning from them to fill in the gaps where I failed you as a teacher, or other teachers did before me. Start where you will, every part of this book will be useful if you intend to build a useful app, and consider that the best apps are built with the end user in mind: Know your customer. Now you know me, you know this book, and you are ready to begin. To start, grab a computer (even the cheapest laptop from a box store, Amazon, or an old desktop works, and set it up in a way that works for you. How to read this book: Text highlighted, denotes that the text belongs in a command prompt, where you will write the code you run. The command prompt is heavily keyboard focused and requires little to no clicking, speeding up your workflow and making things easier on you. Getting started: Let's dive in. We'll start by building code on a local machine and begin without building a website connected to the internet. This is safer to start with, costs nothing, and is easy for you. Depending on your operating system, getting into a bash shell will be a little bit different. For Mac OS, I recommend installing a virtual machine at this point, as you will get the most compatibility with a virtual machine. Various providers such as VirtualBox and Paralells can run a virtual machine for you, though it is also possible to install Ubuntu directly on the machine, if you prefer to use a native environment which is recommended in order to create a fast, streamlined experience. If you are using Linux or Windows, which I recommend, it should be quite easy to create a project. Open your terminal, adjust the sizing as you see fit, and begin following step 2. If you are using Windows, please follow step 1. Step 1: - Windows users only In Windows, open command prompt as administrator and type wsl –install Step 2: - Continue here, or skip step 1 to here if you aren't using Windows In an open terminal, (depending on your OS, called Ubuntu in Windows, Terminal in Mac or Linux, or a similar name), begin by creating a project. We do this with the mkdir command, which creates a directory. If you need to create a directory to store your project, which is recommended, use the cd command to change to the directory and and cd /path/to/directory - The path is the folders (files) that precede your destination directory, your default path is ~ or /home/username (where username is your username). To change to the default directory, type cd or cd ~ mkdir example - Replace “example” with the name of the directory Now you have a working directory for your project. Being as it's so important to have this directory saved in case you need to switch to a different machine or deploy the code you write so it's ready for the web, we will build a script to back up your directory in the next few steps. But building a script takes a bit of code, and code needs to be automated to be as useful as possible. So let's build a script to build scripts first. Let's start by creating the script and making it executable. We'll use sudo, chmod and touch for this, and call the script “ascript”.


sudo touch /usr/bin/ascript
sudo chmod a+x /usr/bin/ascript
sudo nano /usr/bin/ascript
Now we have created the script, made it executable, and are ready to edit it. nano is a text editor that will let you edit text without clicking, which is much easier than using a graphical user interface. To edit a file with nano, use nano and then the path to the file. To make a script that makes a script, it's fairly similar to making our script in the first place. We'll use the same code as above, replacing the name of the script, “ascript” with an argument parameter, $1. This lets us call the script by typing simply sudo ascript newscript, at which point we can create any new script by replacing “newscript” with the name of your script. The code in nano should look like:

sudo touch /usr/bin/$1
sudo chmod a+x /usr/bin/$1
sudo nano /usr/bin/$1
And to close nano, we can hold down the Control key and press X, then Y to denote we are saving the file, and hit return. Now instead of typing these three commands to edit a script, we will be able to type sudo ascript ascript to edit the script again. This works! And any new script can be run easily by calling it in the shell. Let's save our work now: Let's write a backup script to save our new script and then back it up in our project directory, while also backing up the backup script.

sudo ascript backup
Now, in Nano:

sudo cp /usr/bin/backup /path/to/directory/
sudo cp /usr/bin/ascript /path/to/directory/
Where /path/to/directory is the path to the project you created with mkdir. Later on we will learn how to copy repeat paths like this with a loop and a list, which is less code, but for now let's keep it simple and have a few lines. To run this script and backup your code, save the file in nano with Control+X, Y and RETURN, and type the below into your shell

backup
If you are prompted at all for a password while reading this book and following along in the shell, please enter your user password correctly, you will have three tries before you need to re-run the command. You can use the up and down arrows to rerun commands and edit them, should you need to run anything twice. Simple press up and down intermittently to select a command, before editing the command with the right, left arrows and delete key as well as keyboard, and running it with return. Congratulations! You managed to create an awesome backup script that backs up two important shell scripts in your working directory. We might move things around later as the project gets bigger, but this works for now. Let's move on to backing up in the cloud, we'll use GitHub for this (though there are numerous other Git solutions for backup, they are all about the same.) Git is a verision control software that lets you back up edits to your software as you make them to a server, while also enabling you to download entire copies of your software behind a password or key. It is instrumental in saving your software, especially as we migrate to secured linux instances that sometimes break when a single line of code fails, leaving you locked out while your code might not be backed up if you don't get a chance to back it up automatically, which we will cover. If you're not already using an Ubuntu virtual machine at this point, I reccomend using an Ubuntu virtual machine at this point because it will make your life easier when installing all of the packages necessary in order to build a working website and preform deep learning operations on your computer. We will move the code to a web server in the near future, but we want to make sure there are at least a few layers of security behind our web server that are resistant to phishing, and employ a number of linux packages in order to do this. If you still want to use Mac OS, you are welcome to search for and install the necessary packages online, but there may not be alternatives for every package this book or series will cover. Let's add a few commands to commit our work with the backup script by running the command sudo ascript backup.

#  …
git add –all
git commit -m “backup”
git push -u origin master
Once again, Control X to save. Now we need to do some one time configuration for this project. Because it soon will be a Git project, we don't need to type every command every time we deploy from a Git repository, but we'll get the hang of this when we write our deployment scripts. To start, let's make sure we are in the right directory and initialize the git repository and generate ssh keys.

cd /path/to/directory
git init
git branch -m master
ssh-keygen
After we type ssh-keygen, the new key should be saved in the home folder under a folder called .ssh. It is called id_rsa.pub. Let's find this key and copy it. To see it,

cd ~
cat .ssh/id_rsa.pub
Copy the text that is returned by the last command, and create an account with your Git provider (ideally GitHub), before adding the SSH key to your account. Once you have an account, click the upper right menu and enter Settings, before adding your SSH key in SSH and GPG keys under Access in the menu. Select add a SSH key and add yours by pasting it in and giving it a title, before saving and returning to GItHub to create a new repository. This is similar for other Git providers, you will need to read their documentation. In the new repository configuration, give your repository a descriptive name and decide whether you want to publish it, and make sure to configure no files for inclusion yet. Once the repository is created, copy the clone with SSH url, and paste it into the following command.

git remote add git://… (your remote URL)
Now you can move back to your repository with CD, you'll be familiar with this. Try your backup script now with backup Great! Now we can really get coding. Let's install Django now that we have a good grasp on bash and Git. Django will let us automatically back up our software, bash can do this too but Django should have a simpler safer implementation (it can be disabled and configured more easily). To install software in Ubuntu, we will use the sudo apt-get command. First, let's update and upgrade the software we already had. This can be done with sudo apt-get update and sudo apt-get upgrade -y. Next, let's install Python and our virtual environment, the home of our code, with the following command: sudo apt-get install python-is-python3 python3-venv This is all you need to get going with Django in terms of software installs in the Ubuntu instance. For Windows and Linux this should be fairly straightforward, but for Mac you may want to install a virtual machine and Linux on it using a free or paid virtual environment like VirtualBox or Paralells Desktop and recreate the steps above in order to setup an Ubuntu environment. Ubuntu is critical in this case because it is the software the websites run and it enables them to host websites with all of the aforementioned software. Let's dig into the Django. In our directory again, with cd:

python -m venv venv #  Creates the virtual environment where code is stored
source venv/bin/activate #  Activates the virtual enviroment
pip install Django
django-admin startproject mysite . #  Where mysite is the project I am starting in my current directory.
Django is just getting us started, because Django is hosting the web server and is doing everything we need to get a basic local website up and running. Now that we have Django installed, let's edit the settings a bit to make it work how we need. First, let's create a new app

python manage.py startapp feed
You'll notice the first app is called feed. The app should be called whatever you like, and we will create new apps, but the name of each app must be consistent each time the app is referenced in the code. To add a new app, we will always edit the settings.py in the other directory the app created, named in startproject, hereafter app. Using nano,

nano app/settings.py
In the settings, find INSTALLED_APPS and separate the [] into 3 lines. Using four spaces on the empty center line, add 'feed', or the name of your app. This section of the settings.py should look like:

INSTALLED_APPS = [
    'feed',
]
Before we forget, let's test that Django is working. Using the command python manage.py runserver 0.0.0.0:8000, we can run the server and then navigate in a web browser on the computer running the code to http://localhost:8000 and see an example webpage (It Works!) Quit the server with Control C, the same as any other command. Now, let's dig into writing some python code. Django has three main components, all of them run by code entirely. The components are called Model, View and Template, and each is at a higher and lower level respectively before the webpage is delivered to the user. The model is the code that stores information in the database for retrieval, sortation and rendering. The view decides how the model is rendered, manipulated, and modified, almost every view will use a model directly. The template is the HTML code with some extra bells and whistles called template language. The template is rendered by the view where it is filled with Python code and context such as models and information (usuall strings and integers) from the view. Django has other components too, including but not limited to: Settings, which configures the app as we discussed. URLs, which are patterns that the user follows to gain access to specific parts of the web application. Forms, which define how information that is sent to the server is handled and rendered to the database as well as to the user. These are the foundation of processing information on the server side, and can accept any type of information the computer stores, most notably text strings, numbers, and True/False booleans (usually checkboxes). Templates, which are HTML code and template language and bridge the gap between Python and HTML, meaning Python information can be served as HTML code that anyone can access and can secure a website with restricted access, while making Python code accessible to the web and useful for a variety of purposes on a remote device that doesn't need to be near the server. Static files, which are usually JavaScript and it's libraries which the server serves and are linked in with the template. Media files, which the server serves or are externally hosted, or just written to the server before being processed and posted to another server (a bucket) for hosting. Middleware, which is pieces of code that are run at the same time as every view and are considered “included” in the view. Context processors, which process the context of each view and are used to add extra context. Tests, which validate that the user or request passes certain requirements before the view is rendered. Consumers, which dictate how websockets handle and respond to communication. Admin, which is used to register models so they can be manipulated in detail within the Django Admin page, where the database can be administered through a graphical interface. Celery, which defines asynchronous tasks parts of the Django code can begin running before immediately proceeding to the next task or line of code. Django can have many other components, which we will discuss in detail here. There are plenty of ways to make Django more functional, adding WebSockets, which are fast, streamlined communication channels, Celery, which executes asynchronous tasks, and a multitude of other pieces of software for extending Django, especially in the view functions, where most of the code is executed. View functions are key because they usually declare every piece of code that is specific to a specific URL pattern, or a section of the server. First, let's explore view functions. View functions begin with imports denoting code that will be used in the view, and are defined using regular function definitions or classes. The simplest views are defined by the function definition def, and return an HttpResponse with a basic template. Let's start by defining a basic view to return the text “hello world”. Remember that each time you add code after a statement like def, if, while, for, etc, you will need to add 4 spaces for each of the preceeding definitions you would like to apply to your function. We will get into what each of these means soon. From our site's directory, edit the feed/views.py file using nano and add the following lines to the end of the file.

from django.http import HttpResponse

def hello(request):
    return HttpResponse('hello world')
Django's HttpResponse responds with a text string, denoted with the opening and closing '. Every time you pass information to a function or class, like request or a string, you will need to use parenthesis (, opening and closing ). This isn't all we need to see our view yet. Of course, we haven't told the server where the view is exactly, we still need to define a path by which the view should render. Let's start by defining a basic path in app/urls.py, and we will get into path groups later. In app/urls.py, add a line after the import statements after the beginning importing the view we just created.

from feed import views as feed_views
Now, let's define the view pattern. View patterns have three components, the path component, which tells the server where the view exists within the server (the URL path that the user types into the navigation bar to enter the webpage), the view component where the view is specified, and a friendly name for the view so it's easy to retrieve it's pattern when working with a template, especially so it's name can be changed and updated if necessary to make space for another view or take on a more logical name. It makes sense to do things this way and be flexible, because your codebase will be an ever changing environment that needs flexibility and improvisation in order to be valuable and easy to work with. Here's what your view will look like, you can add this to the urlpatterns = [ section of app/urls.py. The view pattern is defined with the three components described above, and a function called path. Your URL patterns are a list, so make sure to always end each item in them with a comma, because this separates each one. Each item should also go on a new line, once again with four spaces before it, just like the app in settings.py. We'll define the first component of the view with an empty string function, in order to create a view that runs on the root directory of the web server. Your urls.py should now look like this:

from feed import views as feed_views

urlpatterns = [
    path('', feed_views.hello, name='hello'),
]
This is the basis for creating a website with Django that is completely static. In order to make a more dynamic website where we can begin caching information, like images, videos, audio and more, we will need to use models, which we will explore next. For now, let's check our code and run the server. To check the code for errors, run:

python manage.py check
If there are any error messages, you should carefully review the changes you made to your app and see if there is anything that needs to be fixed, like an extraneous or lacking space, an extra character, an unclosed string, any typo, any accidentally deleted character, or anything else. Reading through the error message (if you have one), you should be able to see the path to a file you created or edited along with a line number, so look into that file and line and see if you can fix anything that is there. If you have fixed the issue, run the above command again. When your software is ready to run and is working, you will see the output “System check identified no issues.” Now you're ready to go. Run the server with:

python manage.py runserver 0.0.0.0:8000
Now open up a web browser and navigate to http://localhost:8000. You should see the text returned in the parenthesis and quotes of the HttpResponse function in your view. This is just a basic example, but if you made it this far, you understand the basics of how Linux, Bash, Python, and Django work. Let's dig deeper into some database modeling, and explore the power of a Python class in storing information. Then, we will begin to get a grip on HTML and CSS before we make our site fully featured, flexible and secure using JavaScript and Machine Learning. Classes are stored in the models.py of your app. Using nano, edit app/models.py and add a new class. A class is defined with the class definition and is passed a superclass that it inherits from, in this case models.Model. The name of the class comes after the class definition, and after the class definition a : (colon) is used, before the attributes and function definitions tied to the class are denoted below. Our class needs an ID we can use to retrieve it and keep it unique, and it also needs a text field to store some information. Later on we can add a timestamp, files, booleans (true or false definitions that can help our code make decisions about what to do with the model, and can be used to sort it), an instance to tie the model to a user logged into the server, and more. Let's unpack the code below:

from django.db import models #  The import that is used to define our class and it's attributes

class Post(models.Model): #  The definition of our class itself
    id = models.AutoField(primary_key=True) #  The ID of our model, an automatically generated key that will let us query the model, keep it unique, and is useful when we need to interact with the model once it has been created.
    text = models.TextField(default='') #  The attribute our class stores, in this case, some text, defaulting to an empty string.
Close and save the file as we did before to finish. There are many other fields and options we will explore when we update this class as our app evolves, but this is the basic necessities of creating an app to post some text. However, this model won't work alone. As described before, we will need a custom view and custom URL pattern to make this model work, and we will also need a form along with a template. Let's explore the form first. To define a form, edit app/forms.py with nano and add the following lines. We will need two imports, our forms class, as well as the model we created (feed.models.Post), a class definition similar to the model, and a field along with a subclass called Meta that will define the model the form interacts with. The form can also have an initialization function which sets it up based on information in the request, model or otherwise, we will explore this later. Model forms are so useful because they can create a model or also edit a model, so we will use them for both. Let's define one in forms.py below.

from django import forms
from feed.models import Post

class PostForm(forms.ModelForm):
    text = forms.CharField(widget=forms.Textarea)
    class Meta:
        model = Post
        fields = ('text',)
This is the basics of what a form and model look like. This model form can be used to instantiate or edit a post, changing the text it contains. We'll look at integrating this form into a view next. First, let's make the migrations and migrate the database so our code can interact with the model when it runs. To do this, run the following commands:

python manage.py makemigrations
python manage.py migrate
This will take a minute to execute, but once it does, it will allow you to access the model in the views, middleware, or anywhere else in the software. Let's continue by making a view where we can see our model. Edit feed/views.py and add the following code, as noted. You won't need to add anything after the # sign, that code is comments that are used to denote information about the code. We'll start by importing our model in the views, and adding it to a context where we can render it in a template as a list for display. Next, we will add a template where we can render the form and the model with a button to create a new object based on the model and post it to the server. This sounds complicated, so let's just take it step by step. Before we finish the view, let's create a template that just renders the model and make sure we can see it by creating a new post in the shell. Here's how that view should look:

from feed.models import Post
from django.shortcuts import render, redirect
from django.urls import reverse

def feed(request):
    posts = Post.objects.all() #  Query all the posts in the database so far
    return render(request, 'feed/feed.html', {
        'posts': posts,
    })
This all looks pretty simple until we get to the bottom. Render, the value returned by the function instead of in a HTTP response like the previous example, always takes a request as its first input, accepts a context (in this case the posts in the database), which can now be rendered in the template, and returns the template defined in the function. The template is going to be an HTML document with a little bit of a language called Jinja2, which renders Python information into the HTML. To start creating templates, make two directories in feed.

mkdir feed/templates
mkdir feed/templates/feed
Next, edit a template in the directory above, feed/templates/feed, and add the code for this example. Let's look at the template for this example.
 
<!doctype HTML>
<html>
<body>
<legend>Feed</legend>
<hr>
{% for post in posts %}
<p>{{ post.text }}</p>
{% endfor %}
</body>
</html>
 
This is a very simple template. It defines opening and closing HTML tags, a document type tag, a body tag with a legend title, a break tag which adds a small line across the screen, and a for loop that renders each post in the list of posts as a paragraph in the template. This is all it takes to render posts, but there are none in the database yet. Let's create some with the shell. We can run the shell with manage.py

python manage.py shell
Now, let's import our post model

from feed.models import Post
Next, we will create a simple post with a string and exit the shell. The string can be anything, as long it's valid text.

Post.objects.create(text='hello world')
exit()
Lastly, we will need to add a URL pattern to our feed. Because our feed app will use multiple URLs and we want to keep file sizes small, let's create a local urls.py in our feed app that looks like this:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.feed, name='feed'),
]
We will also need to edit the urls.py in the base app, whatever we decided to call it, this was the first directory we created. Edit app/app.py and add the following to the URL patterns

from django.urls import include #  at the top

urlpatterns = [
    # ... previous code here
    path('feed/', include(('feed.urls'), namespace='feed')),
]
Now, when we run the server with python manage.py runserver, we will see the page we created because we have the model, view and template as well as URL pattern, along with items in the database. Next, let's implement the form we created and begin creating our own posts. But before we write too much code, let's make a backup using the script we wrote earlier, backup. Run this script in the shell, wait a few moments, and all of the code will be backed up to our git repository.

backup
Implementing the form is relatively simple. We will import our form, add a post request handler to the view, and save the post in the database before redirecting to the same view. We can use the redirect function we already imported, and another function called reverse to get the URL for the view pattern. We will query this with the string 'feed:feed' because the namespace of the included pattern is feed, and the view is also called feed.

from feed.forms import PostForm

def feed(request):
    posts = Post.objects.all() #  Query all the posts in the database so far
    if request.method == 'POST': #  Handle the post request
        form = PostForm(request.POST) #  Create an instance of the form and save the data to it
        if form.is_valid(): #  Validate the form
            form.save() #  Save the new object
        return redirect(reverse('feed:feed')) #  Redirect to the same URL with a get request
    return render(request, 'feed/feed.html', {
        'form': PostForm(), #  Make sure to pass the form into the context so we can render it.
        'posts': posts,
    })
Now, we'll need to update the template to account for the new form. We can do this by using the
tag in HTML and rendering the form in the HTML template with a submit button. We will also need a csrf token, a token which prevents external sites from posting to the form without first loading a page.
 
<!doctype HTML>
<html>
<body>
<legend>Feed</legend>
<form method=”POST”>
{% csrf_token %}
{{ form }}
<button type=”submit”>New Post</button>
</form>
<hr>
{% for post in posts %}
<p>{{ post.text }}</p>
{% endfor %}
</body>
</html>
 
Let's break this down. There is a new form class, a token, the form itself, and a submit button. Pretty simple, but when we take a look at it, we might want to make it look better. It works, we can post new posts with the form and they are now saved in the database. There are a few things going on here. We use HTML tags to declare that the document is an HTML document, we use a template tag ({% … %}) to render the token for the form, and another, {{ … }} to render the form. We also have a loop to render the text using block tags and a template tag. Block tags are really important because we can define how sections of the template are rendered with them, and template tags are the basis of how we put variables into our code. Now we need to make our app look better, because for now it looks really basic. We can do this by using CSS, either inline, or in classes tied to each object in the document. CSS is really nice because it tells everything on the page how it should look, and can make it look really good. There are a few libraries that can do this, but my personal go to is Bootstrap. Bootstrap can be downloaded from their website, Getbootstrap.com/. Once there, press the button to read the installation docs, and copy the code from the include via CDN section. You will need this code at the top of your HTML document, in a tag called head. Also, let's go ahead and create a base template so we don't need to recreate these links in each template. Make a new directory called templates with mkdir templates, and then edit templates/base.html. It should look like this:
 
<!doctype HTML>
<html>
<head>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
 
Make sure to copy the CSS and JavaScript, the .css and .js files, because we will need the JavaScript to make our site more functional in the future. Now, let's return to the bash shell and run a quick command. Remember, if you ever need to access the virtual environment, type source venv/bin/activate. This will let you install python packages locally in a way that lets Django access them. To give our forms generated by Django bootstrap classes, we will use a python package called crispy forms. We can download this with the following command

pip install django-crispy-forms
Once this is installed, add it to the settings.py

INSTALLED_APPS = [
    #  … previous code here
    'crispy_forms',
]
Now, back in our feed template, we can remove some things. Let's remove the beginning and end of the document and replace it with inheritance from our base template, using extends and the block definition. Also, we will add a template filter import with load and a template filter to the form. Lastly, let's add a bootstrap class to the button on the form to make it look more like a button. That should look like this:
 
{% extends 'base.html' %}
{% block body %}
{% load crispy_forms_tags %}
<form method=”POST”>
{% csrf_token %}
{{ form|crispy }}
<button type=”submit” class=”btn btn-outline-primary”>New Post</button>
</form>
<hr>
{% for post in posts %}
<p>{{ post.text }}</p>
{% endfor %}
{% endblock %}
 
Beautiful! That's quite a bit of code already. Next, we should test it out and make sure we can see that everything looks nice, and also be sure everything is working properly. Run the server as per previous instructions and make sure the site looks and works alright. Great job! You're ready to move on to the next step, in which we will add user login functionality using similar URLs, forms, views and templates. The base template is important, and we will continue to modify it and make changes as needed, but for now let's focus on making our site more secure, by enabling users to log in with a username and passcode, and eventually even more important information that will help keep your app secure and your own account accessible only by you. To do this, we'll need to use the User model built into Django. The user model is a database model, like our post, that can be rendered to log a user into the website. In the future, before we deploy the site to the internet, we will extend this model with other models attributed to it, and build additional security measures for the login that are resistant to phishing. We will begin by using some built in login forms that Django provides. First, let's create a new app that we will use to render the templates and views for the basic login page. We will also create other apps to represent the continued login challenges in order to secure the app, including a pincode, facial recognition, near field communication, external devices, multi factor authentication, and fingerprint recognition. We already talked about starting an app. From our directory, inside the virtual environment, pass manage.py these arguments

python manage.py startapp users
Now, we should have a directory for the new app. Let's start by creating a view in that directory that corresponds to the user login. Django has built in views for user logins, but these won't be suitable for us because we need a custom view, which is preferably done with a definition. In this view, we will start by checking for a post request, pass request.POST to a LoginForm imported from Django, authenticate the user account, and log in the user before redirecting them to our feed app. In users/views.py, add the following code

from django.shortcuts import render, redirect
from django.urls import reverse
from django.contrib.auth.forms import AuthenticationForm, SetPasswordForm
from django.contrib.auth import authenticate, logout
from django.contrib.auth import login as auth_login
from django.contrib import messages

def login(request):
    if request.method == “POST”:
        username = request.POST['username'] #  Get the username and password from the post request
        password = request.POST['password'] #  Authenticate the user
        user = authenticate(username=username, password=password)
        if user:
            auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
            messages.success(request, 'Your password was accepted. Please continue')
            return redirect(reverse('feed:feed'))
        else: messages.warning(request, 'Username or password incorrect. Please try again')
    return render(request, 'users/login.html', {'form': AuthenticationForm()})
This is all you need for a basic login view. Now, let's create a form for the view by extending the base template. We'll start by creating a new directory for templates in the users folder.

mkdir users/templates
mkdir users/templates/users
Now, we should be able to edit users/templates/users/login.html. While we're at it, we'll create a template to allow the user to sign up too.

nano users/templates/users/login.html
Now, in the template,
 
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<form method="POST">
    {% csrf_token %}
    <fieldset class="form-group">
        <legend class="border-bottom mb-4 break">Log In</legend>
        {{ form|crispy }}
    </fieldset>
    <div class="form-group">
        <button class="btn btn-outline-info" type="submit">Login</button>
    </div>
</form>
{% endblock %}
 
This is the basics of a login template. It's really just like the other template in structure, but it looks a little bit different when it's rendered. We can copy this code to build another very similar template called register.html, where we will change the wording and use a new form we build. Let's make the template first. Edit users/templates/users/register.html and add the following code:
 
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<form method="POST">
    {% csrf_token %}
    <fieldset class="form-group">
        <legend class="border-bottom mb-4 break">Create an account</legend>
        {{ form|crispy }}
    </fieldset>
    <div class="form-group">
        <button class="btn btn-outline-info" type="submit">Register</button>
    </div>
</form>
{% endblock %}
 
Now, let's build a form for our user registration and circle back to the views before we upgrade our user logins with a model. We'll make this form basic to start with, but incorporate more details and security features such as agreements and Captcha in the future. Edit the forms with nano users/forms.py, and add the following code.

from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm

class UserRegisterForm(UserCreationForm):
    email = forms.EmailField()

    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']
So we have another form here, which works fairly simply. It's a user register form with a username, email and password, as well as a confirm password field. Note that this form doesn't extend the regular forms.Form class, it's a model form which means it has a Meta. One field is defined just the same, and the class Meta defines the model the form corresponds to the rest of the information that will be written to the form. Most of this already exists in Django's built in UserCreationForm, so we will use that as the basis for the class (passed in the parenthesis). Next, we will examine the view to register a user, now that we have a form and a template. This is a ModelForm, just like the one in the new post view. Edit users/views.py and add the following code:

# … Amounts
from .forms import UserRegisterForm

def register(request):
    if request.method == “POST”:
        form = UserRegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            messages.success(request, 'Welcome to the app, {}.'.format(user.username))
    return render(request, 'users/register.html', {'form': UserRegisterForm})
This is all we need to get a user registered, but we should have more information. We want to know the time the user registered, what time they were last on the site, some information about them, like a biography, timezone, etc. Also, we will need to update our feed model, Post, to account for the User model and attribute posts to each user. In order to do that, we will update the models.py in both apps. Let's start by editing the feed model. It should look like this now:

from django.db import models # … Amounts
from django.contrib.auth.models import User

class Post(models.Model):
    id = models.AutoField(primary_key=True)
    author = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='posts') #  Add in this line
    text = models.TextField(default='')
Pay attention to the second line that was added to the file. This is a foreign key, which will attribute each post to a single user per post, so we can make sure that we save the posts on a user-per-user basis and no post can be made without attributing it to a user. We define this foreign key with the class it represents, a delete argument to ensure posts are deleted with users, null and blank arguments to make sure we can remove the user if necessary, and to accommodate for the lack of a user on posts we already created, and a related name, which we can use to refer to the post objects the user creates. This related name, unlike post.author, the author of the post, gives us user who posted the post itself. We can now get the posts a user made by running user.posts.all(), or author.posts.all(). Now, let's make our logins more resilient. We can already make our site much less vulnerable to phishing by simply rate limiting the number of times we will permit a login to the site, this is quite easy. Let's also begin to store some information about each user before as we continue to develop our app. Editing users/models.py, add the following code.

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True, related_name='profile')
    account_created = models.DateTimeField(default=timezone.now)
    last_seen = models.DateTimeField(default=timezone.now)
    can_login = models.DateTimeField(default=timezone.now)
    preferred_name = models.CharField(max_length=20,default='', null=True, blank=True)
    bio = models.TextField(blank=True, default='')
Note that this model is fairly similar to the Post model. We have an additional import, timezone, which will allow us to set defaults on the datetime fields, and we also have a CharacterFeild and TextField like the post. Using all of these timestamps helps us secure the site and understand its use, and the text fields let us render information about each user, or author, on the website. The OneToOneField should be the only minor consideration, it behaves exactly the same as a ForeginKey but with only one per subsequent model. This way, the user only has one profile, while they may have many posts. Now, let's improve our login and register views to account for the profile. First, edit users/views.py and focus on the register view:

#  … imports
from .forms import UserRegisterForm

def register(request):
    if request.method == “POST”:
        form = UserRegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            Profile.objects.create(user=user) #  Make sure to add this line, to create a profile for the user
            messages.success(request, 'Welcome to the app, {}.'.format(user.username))
    return render(request, 'users/register.html', {'form': UserRegisterForm})
This simply creates a profile for the user, without filling in any of the information. Now, we want to make sure the user account can't be logged into too often, or at least passwords can't be tried too often, so let's update the login view.

# … Amounts
from .models import Profile
from django.utils import timezone
import datetime

def login(request):
    if request.method == “POST”:
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user and user.profile.can_login < timezone.now(): #  Note that we now check if the user can log in
            auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
            messages.success(request, 'Your password was accepted. Please continue.')
            return redirect(reverse('feed:feed'))
        else: #  If the login wasn't successful,
            messages.warning(request, 'Username or password incorrect. Please try again.')
            user = User.objects.filter(username=username).first() #  This is the part where we update the users profile 
            if user: 
                profile = user.profile
                profile.can_login = timezone.now() + datetime.timedelta(seconds=15) #  So they can't log in again for a few seconds
                profile.save()
    return render(request, 'users/login.html', {'form': AuthenticationForm()})
This is the basic fundamental of security. Make sure the site isn't vulnerable to someone simply trying every possible password combination, or even a few of them at the same time. This won't be frustrating to the ordinary user who knows their passcode and just logs in on a few devices, but it will keep numerous phishing robots out of the app. Note that we added an if statement with a variable, can_login, that should be a time in the past, and update it with each unsuccessful login using the same username. This way, a malicious user won't be able to guess a password anywhere near as quickly. The number of seconds in the datetime.timedelta() can be updated too, and the website will be more resilient yet slightly less usable with more seconds. I recommend 15 to start with. Remember, we built a backup script to save our work, so let's go ahead and back up what we have so far to make sure we have everything saved. Run the command:

sudo backup
Once again, this will save your work so far. I recommend running frequent backups to save your work, and you might even want to run a backup job automatically. You can do this using a UNIX utility called cron. To activate this utility, run the following command and enter your password:

sudo crontab -e
If you haven't already select option 1 for nano, the text editor you should already be familiar with, and scroll to the bottom of the file using the arrow keys. Add the following line:

0 * * * * sudo backup
Cron uses the format minute, hour, day of month, month, day of week, where a * or a number represents when to run the command. Using a 0 for the minute and * for the rest of the options, we can run a command on the first minute of every hour at the start of the minute. This lets us back up the code automatically. All of cron's jobs when executed with sudo run as root, so we won't need to type in a password every hour. To make it easier to back up our code without using a password, let's disable the password for our backup command. We will do this by executing the following command and entering a password:

sudo visudo
Now, let's scroll to the bottom of the file and add another line:

ALL ALL=NOPASSWD: /bin/backup
This lets us run the command “backup” as any user, without a password. The format for this is easy, just prefix the line with “ALL ALL=NOPASSWD: /bin/” and end with the command, for example /bin/backup, which exists in /usr/bin/. Now, let's start working with email. Email is really important for websites, because it's a way to keep a website more secure, verify users are real people, and even market products or services to customers. Many people who frequent the internet check their email daily, and receive all sorts of marketing email about products and services they are interested in. There are a few options when it comes to enabling email on a Django website, and you're welcome to pick whichever works best for you. First, you can pay for an email service that will enable you to send email from your domain and requires minimal code. There are many services that offer this, such as Google Workspace, Sendinblue, Mailgun, and more. Otherwise, you are well off building your own email service within your server from scratch. I recommend this option, even though it's more code and may require special hosting. You won't be able to start a mail server from your home computer most likely, so let's go ahead and examine the configuration and code to send email before we start a server in the cloud and create our own mail server within. First, edit settings.py with the following command:

nano app/settings.py
Where app is the name of the app you created with startapp. Add the following lines:

SITE_NAME = 'Django App'

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'localhost'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_ADDRESS = username@server.com'
EMAIL_HOST_USER = 'username'
EMAIL_HOST_PASSWORD = config['EMAIL_HOST_PASSWORD']
DEFAULT_FROM_EMAIL = '{} <{}>'.format(SITE_NAME, EMAIL_HOST_USER)
Make sure to change these when you are ready to deploy your app, we will revisit this later. The EMAIL_ADDRESS setting should be the email you would like to send from, and the password (EMAIL_HOST_PASSWORD) should be set to the password you generate for the server. I load the password in from a config file to keep it out of the code using the following logic, above these lines in settings.py:

import os
import json
with open('/etc/config.json') as config_file:
    config = json.load(config_file)
Then, I have set up a JSON file with the config in /etc/config.json using nano as follows. To edit the file:

sudo nano /etc/config.json
Add the following lines:

{
	“EMAIL_HOST_PASSWORD”: “<some password here>”
}
We will continue to edit the config file and add all of the passwords and keys we will use in the app. For now, let's quickly examine how to send email using Python. First, let's create a template for a verification email we can send to our users, and put it in the user templates directory. This template will be written in HTML.

nano users/templates/users/verification_email.html
 
<h1>Django App - Verify Your Email</h1>
<p>Dear {{ user.username }},</p>
<p>To verify your email, please <a href="{{ base_url }}{% url 'users:activate' uidb64=uid token=token %}">click here</a>.</p>

<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ base_url }}{% url 'users:activate' uidb64=uid token=token %}</p>

<p>The link will expire in 30 minutes.</p>
<p>If you have not requested a verification email you can simply ignore this email.</p>
<p>See you there,</p>
<p>Daisy</p>
 
This email is fairly simple. It takes a context of a user, the base URL for the site, and a user ID and token which are used to verify the user's email. Make sure to define the base URL in settings.py before we write some Python code to render the template. Go ahead and add the following lines to app/settings.py, near the beginning.

SITE_NAME = 'Django App'
PROTOCOL = 'https'
DOMAIN = 'example.com'

BASE_URL = PROTOCOL + '://' + DOMAIN
Eventually, when your site is ready for the internet and you deploy it, you will want to define your domain as the domain name you buy to represent the site. This is the name that you will type in the navbar in order to access your site. For now, you can leave the domain blank or use a placeholder. You will also want to change the SITE_NAME to a name you want to give your site, of your choosing. Before we send email, let's create a token generator so we can have an account activation token that never expires. We can do this by building and importing an account activation token that looks like the following. Edit the file:

nano users/tokens.py
Add the following code:

from django.contrib.auth.tokens import PasswordResetTokenGenerator
import six
class TokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            six.text_type(user.pk) + six.text_type(timestamp)
        )
account_activation_token = TokenGenerator()
unsubscribe_token = TokenGenerator()
This basic token generator generates a token we can send the user in a URL and the user can use to verify their email and activate their account. Next, let's see how to send an email. Using nano, edit users/email.py.

nano users/email.py
Sending the verification HTML email will look like this:

from django.contrib.auth import get_user_model
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.encoding import force_bytes
from django.core.mail import EmailMultiAlternatives
from django.shortcuts import render
from .tokens import account_activation_token
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.template import Template, Context
from django.conf import settings
import traceback

def send_verification_email(user):
    User = get_user_model()
    mail_subject = '[{}] Activate your account.'.format(settings.SITE_NAME)
    html_message = render_to_string('users/verification_email.html', {
        'user': user,
        'domain': settings.DOMAIN,
        'protocol': 'https',
        'uid': urlsafe_base64_encode(force_bytes(user.pk)),
        'token': account_activation_token.make_token(user),
    })
    send_html_email(user, mail_subject, html_message)
This is fairly simple. We import the functions we need to send the email, render the email with templates, and our settings, and then we define the email by the template name and send it to the user using a function. You'll notice we haven't defined the function to send the mail, send_html_email, yet, so let's write this below the code we already added to users/email.py

def send_html_email(user, mail_subject, html_message):
    to_email = user.email
    username = user.username
    if to_email == '':
        return None
    unsub_link = settings.BASE_URL + user.profile.create_unsubscribe_link()
    html_message = html_message + "<p><a href=\"" + unsub_link +  "\" + title=\"Unsubscribe from " + settings.SITE_NAME + " emails\">Unsubscribe</a></p></body></html>"
    msg = EmailMultiAlternatives(mail_subject, strip_tags(html_message), settings.DEFAULT_FROM_EMAIL, [to_email], headers={'List-Unsubscribe' : '<' + unsub_link + '>'},)
    msg.attach_alternative(html_message, "text/html")
    profile = user.profile
    try:
        msg.send(fail_silently=False)
        if not profile.email_valid:
            profile.email_valid=True
            profile.save()
    except:
        profile.email_valid=False
        profile.save()
This is a bit more complex, and we aren't ready to run all of this code yet. Notice we are defining an unsub_link, the link the user can use to unsubscribe from our emails. This is important, because users will need to be able to opt out of our emails unless they want to see them, at any time. We also add a text alternative to our message, which is the HTML message stripped of HTML tags. Lastly, we check if the email sent, and if it didn't, we mark in the user's profile that their email isn't valid. Let's move back to the user models so we can make this all work. We need to define a function to generate a link to unsubscribe, and define a boolean field to mark that the user's email is not valid. First, add the following imports to the top of users/models.py

nano users/models.py

#  …
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.urls import reverse
Next, let's add functions to the user model to make the token and check the token used to activate the email, as well as the field to save whether the user is successfully receiving their mail. In users/models.py again, add the following code to the end of the model (indented code)

#  …
    email_valid = models.BooleanField(default=True)
    
    def make_token(self):
        return TimestampSigner().sign(self.user.username)

    def check_token(self, token):
        try:
            key = '%s:%s' % (self.user.username, token)
            TimestampSigner().unsign(key, max_age=60 * 60 * 24 * 30) #  Valid for 30 days
        except (BadSignature, SignatureExpired):
            return False
        return True

    def create_unsubscribe_link(self):
        username, token = self.make_token().split(":", 1)
        return reverse('users:unsubscribe', kwargs={'username': username, 'token': token,})
This is fairly simple, we use a timestampsigner, which is a basic cryptography tool, to create a token which will expire after a certain amount of time, and we also use another function to check if it is valid. We use these tokens twice, once to verify the email, and once for an unsubscribe link. Now that we have these, the last of the work we will need to do is in the views. Within users/views.py, let's add views to verify the email address, and to unsubscribe.

nano users/views.py
First, add the following imports. I threw in a few extra so we won't have to import more items again later.

from django.contrib.auth import logout
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.models import User
from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
import json
import requests
import datetime, traceback
from django.contrib import messages
from .models import Profile
from django.utils import timezone
from django.views.decorators.cache import never_cache
from .email import send_verification_email #  Make sure to import the verification email sending function
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.utils.decorators import method_decorator
from django.http import HttpResponseRedirect
from django.conf import settings
from django.utils import timezone
import datetime
import pytz
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.contrib.auth.tokens import default_token_generator
from django.utils.http import urlsafe_base64_decode
from .tokens import account_activation_token
You may already have some of these imports, but it doesn't hurt to repeat them. You're going to need to import the verification email sending function, as well as account_activation_token from users.tokens, among other imports. Now, at the bottom of the file, add the following code:

def unsubscribe(request, username, token):
    user = get_object_or_404(User, username=username)
    if((request.user.is_authenticated and request.user == user) or user.profile.check_token(token)):
        #  unsubscribe them
        profile = user.profile
        profile.subscribed = False
        profile.save()
        return render(request, 'users/unsubscribe.html')
    #  Otherwise redirect to login page
    messages.warning(request,f'Your unsubscribe link has expired. Please log in to unsubscribe.')
    next_url = reverse('users:unsubscribe', kwargs={'username': username, 'token': token,})
    return HttpResponseRedirect('%s?next=%s' % (reverse('login'), next_url))

def activate(request, uidb64, token):
    try:
        uid = force_str(urlsafe_base64_decode(uidb64))
        user = User.objects.get(pk=uid)
    except(TypeError, ValueError, OverflowError, User.DoesNotExist):
        user = None
    ip = get_client_ip(request)
    if user is not None and account_activation_token.check_token(user, token):
        user.profile.email_verified = True
        user.profile.save()
        user.save()
# Sendwelcomeemail (request, user)
        messages.success(request, f'Thanks for confirming your email! You can now log into your account, and a welcome email has been sent to you.')
        return redirect(user.profile.create_face_url())
    else:
        messages.success(request, f'Your activation link has expired. Please request a new activation link.')
        return redirect('verify:verify')

def resend_activation(request):
    if request.method == 'POST':
        form = ResendActivationEmailForm(request.POST)
        email = request.POST['email']
        try:
            user = User.objects.get(email=email)
            send_verification_email(user)
            messages.success(request,'Your verification email sent. Please click the link in your email to verify your account.')
            return redirect(reverse('verify:verify'))
        except:
            messages.warning(request,f'Your email is not correct. Please try again.')
    else:
        form = ResendActivationEmailForm()
    return render(request,'users/resend_activation.html',{'form': form, 'title': 'Resend Activation', 'small': True})
This is a lot of code. Let's break it down. The first function, clean and simple, unsubscribes the user from the mailing list. The second function activates their email, and you'll notice I added a commented function, sendwelcomeemail. You are welcome to use an email template and function definition to send a welcome email, I just haven't yet. The last function I threw in is important, because activation emails expire. Therefore, we will need to resend the activation email some of the time. We can use a basic form for this, and call the function to send the verification email. Before we do this, let's make sure it's getting sent in the first place, by adding a function call to the register view. Add this line just before the redirect in the register view, def register, in users/views.py.

nano users/views.py

# … (AFTER) Def Register (Request):
            send_verification_email(user)
#  … (before) redirect(
You don't need to add the first and last lines in that code snippet, just make sure the register view sends the verification email to the user. It should look like this:

# … Amounts
from .forms import UserRegisterForm

def register(request):
    if request.method == “POST”:
        form = UserRegisterForm(request.POST)
        if form.is_valid():
            user = form.save()
            send_verification_email(user) #  Make sure to add this line!
            messages.success(request, 'Welcome to the app, {}.'.format(user.username))
    return render(request, 'users/register.html', {'form': UserRegisterForm})
Now, we'll need to add a form to resend the activation email. In users/forms.py, add the following form:

# … (Amounts)
class ResendActivationEmailForm(forms.Form):
    email = forms.EmailField(required=True)
We will also need a template corresponding to this resend email activation form. Let's add this template in. Edit the file:

nano users/templates/users/resend_activation.html
Next, add the following code to the file.

{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
        <form method="POST">
            {% csrf_token %}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Resend activation email</legend>
                {{ form|crispy }}
            </fieldset>
            <div class="form-group">
                <button class="btn btn-outline-secondary" type="submit">Resend activation email</button>
            </div>
        </form>
{% endblock %}
Whew, that's a lot! Now, when we deploy the code to our server, we will be able to send HTML email and activate user accounts with a click in the email. We also might want to send a simple welcome email, so let's see how to do that. Back in users/email.py, add the following code:

def sendwelcomeemail(user):
    User = get_user_model()
    html = open('{}/users/welcome_email.html'.format(settings.BASE_DIR)).read()
    subject = 'Welcome to ' + settings.SITE_NAME + ', {{ username }}!'
    template = Template(html)
    subjtemplate = Template(subject)
    context = Context({'username': user.username, 'base_url': settings.BASE_URL, 'model_name': 'Daisy Holton, 'site_name': settings.SITE_NAME})
    renderedtemplate = template.render(context)
    subjcontext = Context({'username': user.username})
    subjrenderedtemplate = subjtemplate.render(subjcontext)
    send_html_email(user, subjrenderedtemplate, renderedtemplate)
Also, we will need a template to render all of this information. On my website, the template looks like the below, but you are welcome to format it however you like.
 
<html>
<body>
<h3>Welcome to {{ site_name }}</h3>
<p>Hello {{ username }},</p>
<p>We are happy to see you here! Thank you for joining {{ site_name }} and being a part of the fun. To get started, here are a few things you can do after you verify your identity.</p>
<ol>
    <li><a href="{{ base_url }}/" title="Use the app">Use the app</a>. This is the main page of {{ site_name }}</li>
    <li><a href="{{ base_url }}/feed/profile/Clementine/" title="See my profile">Visit my private {{ site_name }} profile</a>. This is a page for anyone wanting to get to know me.</li>
    <li><a href="{{ base_url }}/feed/profiles/" title="See all profiles currently on the site">More profiles</a>. You can find these people on the site, and see their content.</li>
    <li><a href="{{ base_url }}/feed/all/" title="See everything on {{ site_name }}">See all posts here</a>. This is the private front page of {{ site_name }}.</li>
</ol>
<p>There is even more on the site, so feel free to visit and see what you find. You can share the site with any of the social buttons on each page. I hope you enjoy your time with {{ site_name }}! Thanks for being here.</p>
<p>With much love,</p>
<p>{{ model_name }}</p>
<a href="{{ base_url }}" title="{{ site_name }}">{{ base_url }}</a>
 
Note that we don't have closing body or html tags, because we add these in when we add the HTML unsubscribe link. These are important, but we don't want to define them twice. So what's next? We have come a long way. Really, we should be ready to deploy the site to a server. We can add the @login_required decorator and make our views secure, take user signups, send compliant email, and cache information, which is the basis of what a website needs to do to stay relevant. We'll add a few more useful features, and then build a basis for deploying our code to a remote server, setting up a mail server, domain configuration, and filters to make our site secure and appropriate. We'll also need a password reset view, so let's add that in really quick. Django's built in password reset view is broken in some functions, but we'll look at how to write our own view, email template, forms, and URL patterns. Here's what the view looks like, in users/views.py

# ... amounts
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.forms import SetPasswordForm
from django.utils.http import urlsafe_base64_decode

def password_reset(request, uidb64, token):
    user = get_object_or_404(User, id=urlsafe_base64_decode(uidb64))
    if request.method == 'POST':
        form = SetPasswordForm(user, request.POST)
        if form.is_valid() and default_token_generator.check_token(user, token):
            form.save()
            messages.success(request, 'Your password has been reset.')
        elif not form.is_valid():
            messages.warning(request, 'Your passwords do not match, or do not meet the requirements. Please try again.')
            return redirect(request.path)
        else:
            messages.warning(request, 'Your password reset link has expired. Please create a new one.')
        return redirect(reverse('users:login'))
    return render(request, 'users/password_reset_confirm.html', {
        'title': 'Reset your Password',
        'form': SetPasswordForm(user)
This form is built in to Django, but we'll need a template to confirm the password reset, users/templates/users/password_reset_confirm.html
 
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
        <form method="POST">
            {% csrf_token %}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Reset Password</legend>
                {{ form|crispy }}
            </fieldset>
            <div class="form-group">
                <button class="btn btn-outline-info" type="submit">Reset Password</button>
            </div>
        </form>
{% endblock content %}
 
We also have a template to send a password reset email, with a simple form, in users/templates/users/password_reset.html
 
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
        <form method="POST">
            {% csrf_token %}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Reset Password</legend>
                {{ form|crispy }}
            </fieldset>
            <div class="form-group">
                <button class="btn btn-outline-info" type="submit">Request Password Reset</button>
            </div>
        </form>
{% endblock content %}
 
The template for the email itself is simple, it is a basic HTML file rendering a link to reset the password, in users/templates/users/password_reset_email.html. Django will automatically interpret this file.
 
<h1>Uglek - Reset Your Password</h1>
<p>Hello,</p>
<p>To reset your password, please <a href="https:/uglek.com{% url 'password_reset_confirm' uidb64=uid token=token %}">click here</a>.</p>
<p>Alternatively, you can paste the following link into your browser:</p>
<p>https://uglek.com{% url 'password_reset_confirm' uidb64=uid token=token %}</p>
<p>If you have not requested a password reset you can simply ignore this email.</p>
<p>Thanks for joining us,</p>
<p>Daisy</p>
 
We'll also need two more templates. The first is to confirm that the email has been sent. Views for these are already in Django, so we just need to address them in the urls.py. This template is located at users/templates/users/password_reset_done.html
 
{% extends 'base.html' %}
{% block content %}
  <div class="media-body">
    <div class="alert alert-info">
        An email has been sent with instructions to reset your password.
    </div>
  </div>
{% endblock content %}
 
And lastly, to confirm that the password reset is complete, users/templates/users/password_reset_complete.html
 
{% extends 'base.html' %}
{% block content %}
 <div class="media-body">
    <div class="alert alert-info">
        Your password has been set.
    </div>
    <a href="{% url 'users:login' %}">Sign In Here</a>
  </div>
{% endblock content %}
 
Now, we need URL patterns for these views. In users/urls.py, add the following URL patterns:

urlpatterns = [
    # ... Previous URLs here
    path('password-reset/',
         auth_views.PasswordResetView.as_view(
             template_name='users/password_reset.html',
             html_email_template_name='users/password_reset_html_email.html'
         ),
         name='password_reset'),
    path('password-reset/done/',
         auth_views.PasswordResetDoneView.as_view(
             template_name='users/password_reset_done.html'
         ),
         name='password_reset_done'),
    path('password-reset-confirm/<uidb64>/<token>/',
         auth_views.PasswordResetConfirmView.as_view(
             template_name='users/password_reset_confirm.html'
         ),
         name='password_reset_confirm'),
    path('password-reset-complete/',
         auth_views.PasswordResetCompleteView.as_view(
             template_name='users/password_reset_complete.html'
         ),
         name='password_reset_complete'),
]
Four templates, that's a lot! But now we can be sure to be able to reset the user's password any time we need to, all from the web browser. I understand this is a lot of code. If it seems a little bit over your head, that's ok. You will improve, your understanding will improve, and you will become much more competent with code very soon. If you are totally lost, I recommend coming back to this software later on after working on a self-paced learn to code course online. These are usually free to get started, and will guide you through everything you need to be successful when you come back to this project. If you feel like you are ready to continue, read on, next, we will cover deploying your code to a remote server and setting up a mail server, as well as automating your deployment using bash so you can always set up a new project with a few simple commands. The last thing we need to do before deploying to a remote server is make our site a little bit more secure. You'll notice that the login view only takes a username and password, and there's no multi factor authentication or one time code. This is an easy fix, and with the same code, we can make our site send text messages and even be responsive to text messages sent to the server. To start, we will go back into the user models and add a timestamp signer that will represent each login. We will also add a unique, rotating identifier to the user model that will be used to add extra security to our login. Editing the user models, users/models.py, add the following code:

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
#  Make sure to import the uuid, timestamp signer and URL generator (reverse)
import uuid
from django.core.signing import TimestampSigner, BadSignature, SignatureExpired
from django.urls import reverse

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True, related_name='profile')
    account_created = models.DateTimeField(default=timezone.now)
    last_seen = models.DateTimeField(default=timezone.now)
    can_login = models.DateTimeField(default=timezone.now)
    preferred_name = models.CharField(max_length=20,default='', null=True, blank=True)
    bio = models.TextField(blank=True, default='')
    #  Add this code here
    uid = models.CharField(max_length=32, default=uuid.uuid4, null=True, blank=True)
    mfa_enabled = models.BooleanField(default=False)
    enable_mfa = models.BooleanField(default=False)
    phone_number = models.CharField(default='', null=True, blank=True, max_length=15)
    verification_code = models.CharField(default='', null=True, blank=True, max_length=15)
    verification_code_length = models.IntegerField(default=6)
    mfa_code_expires = models.DateTimeField(default=timezone.now)
    mfa_attempts = models.IntegerField(default=0)

    def make_auth_token(self):
        return TimestampSigner().sign(self.uid)

    #  And add this function
    def create_auth_url(self):
        username, token = self.make_auth_token().split(":", 1)
        return reverse('users:mfa', kwargs={'username': username, 'token': token,})

    def check_auth_token(self, token):
        try:
            key = '%s:%s' % (self.uid, token)
            TimestampSigner().unsign(key, max_age=60 * settings.AUTH_VALID_MINUTES) # Valid for 3 mins
        except (BadSignature, SignatureExpired):
            return False
        return True
Make sure your users/models.py looks like this, besides the comments (code on the lines with #). Breaking this down, it's simple. We have a few imports, a TimestampSigner which is a cryptographic utility that can generate a secure code and verify it in order to make sure it is valid, only been used once, and not older than a certain number of seconds. We also use a UUID, which is a unique identifier that identifies our user in the signing of the token, and in the URL where the token is sent to the user. We'll use this basic cryptography to build a two factor authentication view. Before we do anything else, let's run the migrations so our user models are updated. In the directory with manage.py, run the following commands to make and complete the migrations.

source venv/bin/activate
python manage.py makemigrations && python manage.py migrate
This is important because every time we make changes to the models, we will need to create the tables and update the database with defaults before we can actually use the models. Next, let's improvise our login view to redirect to a secondary authentication view. In users/views.py, remove the login function and redirect to the URL we just generated in the user models.

# … Amounts

def login(request):
    if request.method == “POST”:
        username = request.POST['username']
        password = request.POST['password']
        user = authenticate(username=username, password=password)
        if user and user.profile.can_login < timezone.now(): #  Note that we now check if the user can log in
            #  Remove the auth_login function that was here
            messages.success(request, 'Your password was accepted. Please continue.')
            if user.profile.mfa_enabled:
                return redirect(user.profile.create_auth_url()) #  Note we redirect to a new URL here
            else: #  If the user isn't using mutli-factor authentication, just log them in.
                auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
                return redirect('feed:feed')
        else: #  If the login wasn't successful,
            messages.warning(request, 'Username or password incorrect. Please try again.')
            user = User.objects.filter(username=username).first() #  This is the part where we update the users profile 
            if user: 
                profile = user.profile
                profile.can_login = timezone.now() + datetime.timedelta(seconds=15) #  So they can't log in again for a few seconds
                profile.save()
    return render(request, 'users/login.html', {'form': AuthenticationForm()})
So this is pretty simple, we now have a way to redirect to the two factor authentication view when we create it. We also have a fallback in case the user hasn't added a phone number. We will add a basic view to add a phone number soon and log in with a text message soon. First, we need an easy way to send a text message from our code. To do this, we can choose from a number of APIs, but the easiest one in my opinion is Twilio. They also offer good pricing for smaller projects, as well as bulk discounts. Create an account on Twilio.com, fill in some details about your project, buy a phone number, and copy your API keys to your settings.py. Then, add this code under a new file, users/sms.py.

nano users/sms.py

#  Import all of the necessary packages
from django.utils import timezone
import random
import datetime
from django.conf import settings
from feed.middleware import get_current_request
from django.contrib import messages
import traceback

account_sid = settings.TWILIO_ACCOUNT_SID
auth_token = settings.TWILIO_AUTH_TOKEN
source_phone = settings.PHONE_NUMBER

#  This code sends the text with twilio
def send_text(target_phone, text):
    from twilio.rest import Client
    try:
        client = Client(account_sid, auth_token)
        if len(target_phone) >= 11:
            message = client.messages.create(
                to=target_phone,
                from_=source_phone,
                body=text)
    except:
        print(traceback.format_exc())

#  A helper function to get a number with so many digits
def get_num_length(num, length):
    n = ''
    for x in range(length):
        n = n + str(num)
    return int(n)

#  Send the text to verify the user
def send_verification_text(user):
    length = user.profile.verification_code_length
    code = random.randint(get_num_length(1, length), get_num_length(9, length));
    user.profile.verification_code = code
    user.profile.mfa_code_expires = timezone.now() + datetime.timedelta(minutes=3)
    user.profile.save()
    send_user_text(user, "Your verification code for {} is {}".format(settings.SITE_NAME, str(code)))

#  Send a user any text with this function
def send_user_text(user, text):
    send_text(user.profile.phone_number, text)

#  Validate the code with this function
def check_verification_code(user, code):
    user.profile.mfa_attempts += 1
    result = user.profile.verification_code != None and code != '' and user.profile.verification_code == code and user.profile.mfa_code_expires > timezone.now() and user.profile.mfa_attempts <= 3
    if user.profile.mfa_attempts < 3 and result:
        user.profile.verification_code_length = 6
    elif user.profile.mfa_attempts > 2 and not result:
        user.profile.verification_code_length = 8
    user.profile.save()
    return result

#  Validate the time
def check_verification_time(user):
    result = user.profile.mfa_code_expires > timezone.now()
    return result
Be sure to change your settings appropriately, adding these lines with your keys:

#  Make sure to copy these from your Twilio dashboard
TWILIO_ACCOUNT_SID = “<your sid>”
TWILIO_AUTH_TOKEN = “<your token>”
PHONE_NUMBER = “<your twilio phone number>”
SITE_NAME = “<Your site name>”
AUTH_VALID_MINUTES = 3 #  The number of minutes the TFA page is active once instantiated
First, we will need forms for our two factor authentication views. Editing users/forms.py, add the following code.

# … Amounts
from django import forms

#  A form for entering our phone number
class PhoneNumberForm(forms.Form):
    phone_number = forms.RegexField(regex=r'^\+?1?\d{9,15}$', error_messages = {'invalid': "Phone number must be entered in the format: '+999999999'. Up to 15 digits is allowed."})
    def __init__(self, *args, **kwargs):
        super(PhoneNumberForm, self).__init__(*args, **kwargs)
        self.fields['phone_number'].label = phone_number_label

#  A form for authenticating
class TfaForm(forms.Form):
    code = forms.IntegerField(required=False)
    def __init__(self, *args, **kwargs):
        super(TfaForm, self).__init__(*args, **kwargs)
        self.fields['code'].widget.attrs.update({'autocomplete': 'off'})
    help_texts = {
        'code': 'Please enter the six digit code after sending it to your phone with the button above.'
    }
Next, let's create the views in users/views.py

# … Amounts
from django.http import HttpResponseRedirect
from .forms import PhoneNumberForm, TfaForm

def mfa(request, username, token):
    user = User.objects.filter(profile__uuid=username).first()
    if not user: return HttpResponseRedirect(reverse('verify:age') + '?next=' + request.GET.get('next') if request.GET.get('next') else '/go/' if request.user.is_authenticated and request.user.profile.vendor else '/' if request.user.is_authenticated else reverse('users:login'))
    user = get_object_or_404(User, profile__uuid=username)
    next = request.GET.get('next','')
    if not user.profile.mfa_enabled:
        if not check_verification_time(user):
            user.profile.mfa_enabled = False
            user.profile.enable_two_factor_authentication = True
            user.profile.phone_number = '+1'
            user.profile.save()
            print('Logging in user')
            auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
            messages.warning(request, 'Please enter a valid phone number and verify it with a code.')
            return redirect(reverse('users:mfa_onboarding'))
    if request.method == 'POST':
        form = TfaForm(request.POST)
        code = form.data['code']
        if code and code != '' and code != None:
            token_validated = user.profile.check_auth_token(token)
            p = user.profile
            is_verified = check_verification_code(user, int(code))
            p.mfa_authenticated = is_verified
            if token_validated:
                if is_verified:
                    user.profile.mfa_enabled = True
                    user.profile.save()
                    auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend')
                    p.verfication_code = None
                    p.uid = get_uuid()
                    p.save()
                    messages.success(request, 'You have been authenticated. Welcome.')
                    qs = '?'
                    for key, value in request.GET.items():
                        qs = qs + key + '=' + value + '&'
                    if next != '' and not (next.startswith('/accounts/logout/') or next.startswith('/accounts/login/') or next.startswith('/admin/login/') or next.startswith('/accounts/register/')):
                        return HttpResponseRedirect(ext)
                    elif next.startswith('/accounts/logout/') or next.startswith('/accounts/login/') or next.startswith('/accounts/register/'):
                        return redirect('feed:feed')
                    elif request.META.get('HTTP_REFERER', '/').startswith('/accounts/login/'):
                        return redirect(reverse('feed:feed'))
                    elif not next:
                        return redirect(reverse('feed:feed')
                    else:
                        return HttpResponseRedirect('feed:feed')
                else:
                    messages.warning(request, 'The code you entered was not recognized. Please try again.')
            elif not token_validated:
                messages.warning(request, 'The URL token has expired or was not recognized. Please try again.')
                logout(request)
                return redirect(reverse('users:login'))
            if p.mfa_attempts > 3:
                messages.warning(request, 'You have entered the incorrect code more than 3 times. please send yourself a new code.')
                p.verification_code = None
                p.save()
        elif user.profile.can_send_mfa < timezone.now():
            user.profile.mfa_attempts = 0
            user.profile.can_send_mfa = timezone.now() + datetime.timedelta(minutes=2)
            user.profile.save()
            send_verification_text(user)
            messages.success(request, "Please enter the code sent to your phone number. The code will expire in 3 minutes.")
        else:
            messages.warning(request, 'You are sending too many two factor authentication codes. Wait a few minutes before sending another code.')
    form = TfaForm()
    hide_logo = None
    if user.profile.hide_logo:
        hide_logo = True
    return render(request, 'users/mfa.html', {'title': 'Enter Code', 'form': form, 'xsmall': True, 'user': user, 'hide_logo': hide_logo, 'accl_logout': user.profile.shake_to_logout, 'preload': False})

@login_required
def mfa_onboarding(request):
    if request.method == 'POST':
        form = PhoneNumberForm(request.POST)
        request.user.profile.phone_number = form.data['phone_number'].replace('-', '').replace('(','').replace(')','')
        request.user.profile.mfa_enabled = True
        request.user.profile.enable_two_factor_authentication = True
        request.user.profile.save()
        messages.success(request, 'You have added a phone number to your account.')
        user = request.user
        return redirect(user.profile.create_auth_url())
    form = PhoneNumberForm(initial={'phone_number': request.user.profile.phone_number if request.user.profile.phone_number else '+1'})
    return render(request, 'users/mfa_onboarding.html', {'title': 'Enter your phone number', 'form': form, 'small': True})
We will also need templates for both of these views. Let's add the mfa template first.

nano users/templates/users/mfa.html
Add this HTML code to the template
 
{% extends 'base.html' %}
{% block content %}
{% load app_filters %}
{% load crispy_forms_tags %}
        <form action="{{ request.path }}{% if request.GET.next %}?next={{ request.GET.next }}{% endif %}" method="POST">
            {% csrf_token %}
            <legend class="border-bottom mb-4">Enter Verification Code</legend>
            <p>Step 1: Send the code</p>
	    <i>Never share your code with anyone, as it can be used to access your account temporarily.</i>
	    <div class="form-group">
                <button class="btn btn-outline-primary" type="submit">Send code</button>
            </div>
	    <hr>
	    <p>Step 2: Enter the code</p>
            <fieldset class="form-group">
                {{ form|crispy }}
		<p>Press the enter button to send yourself the code at {{ user.profile.phone_number|securephone }}. Then, enter the code and press enter.</p>
            </fieldset>
            <div class="form-group">
                <button class="btn btn-outline-secondary" type="submit">Enter code</button>
            </div>
        </form>
{% endblock %}
 
This is pretty self explanatory. The form sends either a code or an empty code, and you'll notice in the view we send the code if we receive an empty code. Then we just have two submit buttons, and this way we can send the code with either button. Next, we'll add a simple form to add a phone number.

nano users/templates/users/mfa_onboarding.html
Add the following html:
 
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
        <form method="POST">
            {% csrf_token %}
            <fieldset class="form-group">
                <legend class="border-bottom mb-4">Set Up Two Factor Authentication</legend>
                {{ form|crispy }}
            </fieldset>
            <div class="form-group">
                <button class="btn btn-outline-secondary" type="submit">Add phone number</button>
            </div>
        </form>
{% endblock %}
 
This form is a lot simpler, it just renders the phone number form we created and lets the user add a phone number. This looks really good! As long as everything is properly set up, we should be able to send messages, and log the user in with their phone number as soon as we add the URL patterns. The last thing we need to set up is a profile view so we can make sure the user can change their phone number without being logged in. Also, eventually we will want to add a “stop to quit” option, so the user can text “stop” to opt out of future text messages. Let's add a profile view to the users/views.py. This view will update the user's bio, email, username, and phone number, as well as allow us to enable multi factor authentication. First, we will need two more forms in users/forms.py

# ... amounts
class UserUpdateForm(forms.ModelForm):
    email = forms.EmailField()
    class Meta:
        model = User
        fields = ['username', 'email']

phone_number_label = 'Phone number (no spaces, parenthesis \'(\' or dashes \'-\', numbers beginning with + only)'

class ProfileUpdateForm(forms.ModelForm):
    subscribed = forms.BooleanField(required=False)
    phone_number = forms.CharField(required=False)
    def __init__(self, *args, **kwargs):
        super(ProfileUpdateForm, self).__init__(*args, **kwargs)
    class Meta:
        model = Profile
        fields = ['bio', 'phone_number', 'enable_mfa', 'subscribed']
Next, we can create a view to use both of these forms. Edit users/views.py and add in the view.

#  Add these imports
from .forms import UserUpdateForm, ProfileUpdateForm
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
from .models import Profile
from .mfa import send_user_text

@csrf_exempt
@never_cache
@login_required
def profile(request):
    if request.method == 'POST':
        u_form = UserUpdateForm(request.POST, instance=request.user)
        p_form = ProfileUpdateForm(request.POST,
                                       request.FILES,
                                       instance=request.user.profile)
        if u_form.is_valid() and p_form.is_valid():
            new_phone_number = p_form.data['phone_number']
            u_form.save()
            profile = p_form.save(commit=False)
            profile.phone_number = profile.phone_number.replace('-', '').replace('(','').replace(')','')
            profile.save()
            if new_phone_number != oldprofile.phone_number and oldprofile.phone_number and len(oldprofile.phone_number) >= 11:
                profile.mfa_enabled = True
                profile.save()
                send_text(oldprofile.phone_number, 'Your phone number has been updated to ' + new_phone_number + '. Please refer to texts on that phone to log in. If you didnt make this change, please call us. - {}'.format(settings.SITE_NAME))
            if profile.enable_two_factor_authentication and profile.phone_number and len(profile.phone_number) < 11:
                profile.enable_two_factor_authentication = False
                messages.success(request, f'Two factor authentication can\'t be activated without entering a phone number. Please enter a phone number to enable two factor authentication.')
            profile.save()
            if new_phone_number != oldprofile.phone_number and new_phone_number and len(new_phone_number) >= 11:
                send_user_text(request.user, 'You have added this number to {} for two factor authentication. You can now use your number for two factor authentication. If you didnt make this change, please call us. - {}'.format(settings.SITE_NAME, settings.DOMAIN))
                profile.mfa_enabled = True
                profile.mfa_code_expires = timezone.now() + datetime.timedelta(minutes=3)
                profile.save()
                return redirect(profile.create_auth_url())
            messages.success(request, f'Your profile has been updated!')
            print('Profile updated')
            return redirect('users:profile')
    else:
        u_form = UserUpdateForm(instance=request.user)
        p_form = ProfileUpdateForm(instance=request.user.profile, initial={'phone_number': request.user.profile.phone_number if request.user.profile.phone_number else '+1'})
    context = {
        'u_form': u_form,
        'p_form': p_form,
        'title':'Update Your Profile',
    }
    return render(request, 'users/profile.html', context)
We'll also need a template for this view.

nano users/templates/users/profile.html
 
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load feed_filters%}
{% block content %}
	<h2>Edit Your Profile</h2>  
	<form method="POST" enctype="multipart/form-data" id="profile-form">
          {% csrf_token %}
          <fieldset class="form-group">
              <legend class="border-bottom mb-4 mt-4">Profile info</legend>
              {{ u_form|crispy }}
              {{ p_form|crispy }}
          </fieldset>
          <div class="form-group">
              <button class="btn btn-outline-info" type="submit">Update}</button>
          </div>
	</form>
        <p style="text-color: green;" class="hide" id="posted">Saved</p>

{% endblock content %}
{% block javascript %}
var form = document.getElementById('profile-form');
$('input').change(function(){
	var formdata = new FormData(form);
	$.ajax({
		url: window.location.href,
		type: "POST",
		data: formdata,
		processData: false,
		contentType: false,
		timeout: 1000 * 60,
                success: function(data) {
                  $(posted).removeClass("hide");
		  setTimeout(function() {
			$(posted).addClass("fade-hidden");
			setTimeout(function() {
				$(posted).addClass("hide");
				$(posted).removeClass("fade-hidden");
			}, 2000);
		  }, 2000);
                }
	});
});
{% endblock %}
 
You'll notice this is a fairly simple form, but has some javascript in it that automatically posts the contents of the form as they are updated. This is useful to have, so you are able to make edits without having to press submit every time. Next, we need URLs representing all of these views in the users URL patters. Edit users/urls.py and add this code:

# … Previous code, imports
from django.urls import path
from . import views

app_name='users'

urlpatterns = [
#  … url patterns we previously entered, add the next three lines
    path('mfa/<str:username>/<str:token>/', views.mfa, name='mfa'),
    path('mfa/onboarding/', views.mfa_onboarding, name='mfa_onboarding'),
    path('profile/', views.profile, name='profile'),
]
Now is a good time to test out our project. But first, let's run another backup.

backup
And run the server. Before we deploy to a linux server, it's a good idea to enable two factor authentication on the account. We'll do this going to our profile URL, /users/profile/, and checking the box to enable authentication after entering our phone number, and then submitting the form.

python manage.py runserver localhost:8000
Visit the webpage by going to your web browser, I'm using Google Chrome in this example, and entering the URL https://localhost:8000/accounts/profile/ You will be able to log in if necessary and enable two factor authentication. This project needs a server to run on so it can really send mail. But first, we need a way to see errors. You'll notice that if you run the server in debug mode, with settings.DEBUG equal to True, the server shows errors automatically. To show errors without using debug mode, which is unsafe on a production server, we should add a view for it. The most important errors we need to be able to handle are: Error 500 - A problem with our code Error 404 - A page that wasn't found (broken URL) Error 403 - A permission denied error Let's add an new app to handle these errors, called errors.

python manage.py startapp errors
Add this to the settings.py as we did before, in the INSTALLED_APPS setting, and begin by adding references to some views in app/urls.py, where app is the name of your django project.

handler404 = 'errors.views.handler404'
handler500 = 'errors.views.handler500'
handler403 = 'errors.views.handler403'
This is all we need besides error views, templates and a little bit of middleware. Let's define those as so:

from django.shortcuts import render, redirect
from django.http import HttpResponse
from stacktrace.models import Error
from errors.middleware import get_current_exception
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import user_passes_test
from .logs import get_logs
from face.tests import is_superuser_or_vendor
from django.views.decorators.csrf import csrf_exempt
from errors.highlight import highlight_code
from django.shortcuts import redirect
from django.urls import reverse

#  Create your views here.
@login_required
@user_passes_test(is_superuser_or_vendor)
def logs(request):
    logs = highlight_code(get_logs())
    return render(request, 'errors/live_error.html', {'title': 'Error Logs', 'pagetitle': 'Error Logs', 'notes': 'These are the recent error logs.', 'trace': logs, 'full': True})

@login_required
@user_passes_test(is_superuser_or_vendor)
def logs_api(request):
    logs = highlight_code(get_logs())
    return HttpResponse(logs)

@login_required
def handler404(request, exception):
    if not request.path.endswith('/'): return redirect(request.path + '/')
    return render(request, 'errors/error.html', {'title': 'Error 404', 'pagetitle': 'Error 404', 'notes': 'This page was not found on the server. It may have moved or been deleted.', 'is_404': True})

def handler500(request):
    print(get_current_exception())
    user = None
    if hasattr(request, 'user') and request.user and request.user.is_authenticated:
        user = request.user
    try:
        Error.objects.create(user=user, stack_trace=get_current_exception(), notes='Logged by 500 handler.')
    except: pass
    return render(request, 'errors/error.html', {'title': 'Error 500', 'pagetitle': 'Error 500', 'notes': 'There is a problem with the server, or with a request coming from you. Thank you for your understanding while we get things set up.', 'trace': get_current_exception()})

def handler403(request, exception):
    return render(request, 'errors/error.html', {'title': 'Error 403', 'pagetitle': 'Error 403', 'notes': 'You don\'t have permission to preform this request. If you think this is in error, please contact the server administrator.', 'is_403': True})

def handler400(request, exception):
    return render(request, 'errors/error.html', {'title': 'Error 400', 'pagetitle': 'Error 400', 'notes': 'This was a bad request.'})
Next, let's define the middleware to handle these errors. We will do this by first adding to MIDDLEWARE_CLASSES in settings.py, with the name of our middleware.

MIDDLEWARE_CLASSES = [
    # ... previous middleware
    'errors.middleware.ExceptionVerboseMiddleware,
]
Next, let's add the middleware.

from threading import local
import traceback
from django.utils.deprecation import MiddlewareMixin

_error = local()

class ExceptionVerboseMiddleware(MiddlewareMixin):
    def process_exception(self, request, exception):
        _error.value = traceback.format_exc()

def get_current_exception():
    try:
        return _error.value
    except AttributeError:
        return None

def set_current_exception(exception):
    try:
        _error.value = exception
    except AttributeError:
        print('Attribute error setting exception.')
We add a function to get the current exception by using a threading local, which helps us trace any errors in our code. In terms of templates, we only need one, because we dynamically define the title in the view. The template just needs to render the title and “trace”, our error traceback from the context.

nano errors/templates/errors/error.html
 
{% extends 'base.html' %}
{% block content %}
<h1>{{ pagetitle }}</h1>
<p>{{ trace }}</p>
{% endblock %}
 
This is our simplest template yet, but that's how easy it is to see the errors in our project. Next, let's disable debug in settings.

nano app/settings.py
Find this line where it is set to True, and change it to False

DEBUG = False
Go ahead and backup the app now. We are ready to deploy to a remote linux server, and keep adding features from there.

sudo backup
Before we post this code to a server, we should consider that there may be some issues with the code. Depending on the case, sites that accept information posted to them will have issues with spam being posted and difficulty removing the spam. This shouldn't happen immediately, but if it is happening, we will later examine how to automatically moderate spam on the site and make it tougher for robots to access the site, along with how to deactivate user accounts, and verify a user's identity with a scan of their ID or a biometric scan, like a fingerprint or facial recognition. Looking at the multi factor authentication example we examined, in production, things can be different. Notice how we are rate limiting logins, and expiring tokens. If robots are accessing a site, two factor authentication can be more difficult as they may enter codes at the same time the user is. To combat this, let's use a model in the user models, declaring how we interact with the site when we are authenticating using multi factor authentication with a phone number. We will also add an option to authenticate with email. Start by editing the user models with nano.

nano users/models.py
This is what the model we are adding should look like. We don't need any methods, just variables to store an id, the user, the timestamp, expiration, length and attempts against any multi factor authentication (a code like 123456 sent to a phone or email).

#  A basic token used to log in to the website
class MFAToken(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='mfa_tokens')
    timestamp = models.DateTimeField(default=timezone.now)
    expires = models.DateTimeField(default=timezone.now)
    token = models.CharField(default='', max_length=100)
    length = models.IntegerField(default=6)
    attempts = models.IntegerField(default=0)
    uid = models.CharField(default=uuid.uuid4, max_length=100)
Let's also add a privilege to our user, and we will set it manually for now, before eventually migrating to enlisting privileged users automatically. In the user models, add this line in the Profile:

    vendor = models.BooleanField(default=False)
As with any changes to the database, we need to make migrations and migrate the database any time we edit a models.py file in Django. Remember, to do this we use source first (if it hasn't been used already since the terminal was open) and then python manage.py to make the migrations and migrate.

cd project-directory-you-named #  (if needed)
source venv/bin/activate
python manage.py makemigrations && python manage.py migrate
For now, you can enlist any accounts you have created as vendors by using the shell.

python manage.py shell
from users.models import Profile
p = Profile.objects.get(user__username='Charlotte')
p.vendor = True
p.save()
exit()
Now, let's evolve our multi factor authentication view to use this token. First, we need to modify our MFA helper utilities. Using nano,

nano users/mfa.py

from django.utils import timezone
import random
import datetime
from django.conf import settings
from feed.middleware import get_current_request
from django.contrib import messages
from .email import send_html_email
import traceback
from .models import MFAToken

account_sid = settings.TWILIO_ACCOUNT_SID
auth_token = settings.TWILIO_AUTH_TOKEN
source_phone = settings.PHONE_NUMBER

def send_text(target_phone, text):
    from twilio.rest import Client
    try:
        client = Client(account_sid, auth_token)
        if len(target_phone) >= 11:
            message = client.messages.create(
                to=target_phone,
                from_=source_phone,
                body=text + ' Text STOP to cancel.')
    except:
        messages.warning(get_current_request(), 'There was an error sending the message.')
        print(traceback.format_exc())

def get_num_length(num, length):
    n = ''
    for x in range(length):
        n = n + str(num)
    return int(n)

def send_verification_text(user, token):
    length = user.profile.verification_code_length
    code = random.randint(get_num_length(1, length), get_num_length(9, length));
    token.token = code
    token.expires = timezone.now() + datetime.timedelta(minutes=settings.AUTH_VALID_MINUTES)
    token.save()
    send_user_text(user, "Your verification code for {} is {}".format(settings.SITE_NAME, str(code)))

def send_verification_email(user, token):
    length = user.profile.verification_code_length
    code = random.randint(get_num_length(1, length), get_num_length(9, length));
    token.token = code
    token.expires = timezone.now() + datetime.timedelta(minutes=settings.AUTH_VALID_MINUTES)
    token.save()
    send_html_email(user, "Your verification code for {} is {}".format(settings.SITE_NAME, str(code)), "<p>Dear {},</p><p>Your verification code for {} is {}. Thank you for using this code to secure your account.</p><h2>{}</h2><p>Sincerely, {}</p>".format(user.profile.name, settings.SITE_NAME, str(code), str(code), settings.SITE_NAME))

def send_user_text(user, text):
    send_text(user.profile.phone_number, text)

def check_verification_code(user, token, code):
    token.attempts = token.attempts + 1
    profile = user.profile
    result = (token != None and code != '' and token.token == code and (token.expires > timezone.now()) and token.attempts <= settings.MFA_TOKEN_ATTEMPTS)
    if token.attempts < 3 and result:
        profile.verification_code_length = 6
    elif token.attempts > 1 and not result:
        profile.verification_code_length = profile.verification_code_length + 2
        if profile.verification_code_length > settings.MFA_TOKEN_LENGTH: profile.verification_code_length = settings.MFA_TOKEN_LENGTH
    token.save()
    profile.save()
    return result

#  Authenticate the user using their email or phone number
def mfa(request, username, usertoken):
    token = MFAToken.objects.filter(uid=username, expires__gt=timezone.now() + datetime.timedelta(seconds=30)).order_by('-timestamp').last() #  Filter the token by the value passed in the URL (a UUID)
    if not token: token = MFAToken.objects.create(user=User.objects.filter(profile__uuid=username).first(), uid=username, expires=timezone.now() + datetime.timedelta(seconds=115)) #  If this session hasn't been created, create it
    user = User.objects.filter(id=token.user.id).first() #  Get the user from the token
    if not user and request.user.is_authenticated: return redirect(reverse('feed:home')) #  If they are already authenticated, log them in
    if not user: raise PermissionDenied() #  Deny if no user was found
    next = request.GET.get('next','')
    if not user.profile.enable_two_factor_authentication and user.is_active and user.profile.check_auth_token(usertoken, token): #  Check the auth token
        auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend') #  Log in the user if they are not already logged in
        user.profile.mfa_expires = timezone.now() + datetime.timedelta(minutes=settings.LOGIN_VALID_MINUTES) #  Set an expiration on their multi factor authentication
        user.profile.save()
        return HttpResponseRedirect(next if next != '' else reverse('landing:landing')) #  Redirect the user to the next page
    if not user.profile.mfa_enabled: #  Check if mfa is enabled
        if not check_verification_time(user, token): #  Check the time
            user.profile.mfa_enabled = False #  Clear the phone number
            user.profile.enable_two_factor_authentication = True # Enable mfa
            user.profile.phone_number = '+1' #  Disable the phone number
            user.profile.save() #  Save the profile
            auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend') #  Log the user in if their MFA is not enabled
            messages.warning(request, 'Please enter a valid phone number and verify it with a code.')
            return redirect(reverse('users:mfa_onboarding'))
    if request.method == 'POST' and not fraud_detect(request, True): #  If the request is a post request
        form = TfaForm(request.POST) #  Instantiate the form
        code = str(form.data.get('code', None)) #  Get the code
        if code and code != '' and code != None: #  Make sure it's not empty
            token_validated = user.profile.check_auth_token(usertoken) #  Check the auth token
            p = user.profile
            is_verified = check_verification_code(user, token, code) #  Check the code
            p.mfa_authenticated = is_verified
            if token_validated: #  If everything
                if is_verified: # Is in order
                    user.profile.mfa_enabled = True #  Enable MFA (if not already enabled)
                    user.profile.save()
                    auth_login(request, user, backend='django.contrib.auth.backends.ModelBackend') #  Log in the user
                    face = user.faces.filter(session_key=None).last() 
                    p.mfa_expires = timezone.now() + datetime.timedelta(minutes=settings.LOGIN_VALID_MINUTES)
                    p.save()
                    messages.success(request, 'You have been authenticated. Welcome.')
                    qs = '?'
                    for key, value in request.GET.items(): #  Build a querystring for the next parameter (if any)
                        qs = qs + key + '=' + value + '&'
                    if next != '' and not (next.startswith('/accounts/logout/') or  next.startswith('/accounts/login/') or next.startswith('/admin/login/') or next.startswith('/accounts/register/')):
                        return HttpResponseRedirect(next) #  Redirect
                    elif next.startswith('/accounts/logout/') or next.startswith('/accounts/login/') or next.startswith('/accounts/register/'):
                        return redirect(reverse('/'))
                    elif request.META.get('HTTP_REFERER', '/').startswith('/accounts/login/'):
                        return redirect(reverse('/'))
                    elif not next:
                        return redirect(reverse('/'))
                    else:
                        return HttpResponseRedirect(reverse('verify:age') + '?next=' + request.META.get('HTTP_REFERER', '/'))
                else:
                    messages.warning(request, 'The code you entered was not recognized. Please try again.')
            elif not token_validated: #  If the token was invalid
                messages.warning(request, 'The URL token has expired or was not recognized. Please try again.')
                logout(request)
                return redirect(reverse('users:login'))
            if p.mfa_attempts > 3: #  If there were too many attempts
                messages.warning(request, 'You have entered the incorrect code more than 3 times. please send yourself a new code.')
                p.verification_code = None
                p.save()
        elif user.profile.can_send_mfa < timezone.now():
            user.profile.mfa_attempts = 0
            user.profile.can_send_mfa = timezone.now() + datetime.timedelta(minutes=2)
            user.profile.save()
            if form.data.get('send_email', False): #  Send the email (or text)
                send_mfa_verification_email(user, token)
            else:
                send_verification_text(user, token)
            messages.success(request, "Please enter the code sent to your phone number or email. The code will expire in 3 minutes.")
        elif user.profile.can_send_mfa < timezone.now() + datetime.timedelta(seconds=115):
            messages.warning(request, 'You are sending too many two factor authentication codes. Wait a few minutes before sending another code.')
    form = TfaForm()
    hide_logo = None
    if user.profile.hide_logo:
        hide_logo = True
    if request.user.is_authenticated: return redirect(reverse('/'))
    #  Render the form (for get requests)
    return render(request, 'users/mfa.html', {'title': 'Enter Code', 'form': form, 'xsmall': True, 'user': user, 'hide_logo': hide_logo, 'accl_logout': user.profile.shake_to_logout, 'preload': False, 'autofocus': request.method == 'POST'})
When we are adding in this code, make sure to import the function to send an email. At the top of the file, the user views (with other imports), add

from .mfa import send_verification_email as send_mfa_verification_email
Now, we need to write that function before any of this will work. It should extend our send email function, and simply send an email to the user with the verification code.

nano users/mfa.py

def send_verification_email(user, token):
    length = user.profile.verification_code_length
    code = random.randint(get_num_length(1, length), get_num_length(9, length));
    token.token = code
    token.expires = timezone.now() + datetime.timedelta(minutes=settings.AUTH_VALID_MINUTES)
    token.save()
    send_html_email(user, "Your verification code for {} is {}".format(settings.SITE_NAME, str(code)), "<p>Dear {},</p><p>Your verification code for {} is {}. Thank you for using this code to secure your account.</p><h2>{}</h2><p>Sincerely, {}</p>".format(user.profile.name, settings.SITE_NAME, str(code), str(code), settings.SITE_NAME))
So this all works great, now we have a multi factor authentication system that depends a phone number or email to log in. But we also need a way to remove, or at least hide users who aren't cooperating with our terms. These could be spammers, robots or anyone who doesn't mean well for our work. Take a look at a view I have for monitoring users on my website:

# amounts
from django.contrib.auth.decorators import login_required
from django.contrib.auth.decorators import user_passes_test
from .tests import is_superuser_or_vendor #  We will need to create this test

@login_required
@user_passes_test(is_superuser_or_vendor)
def users(request):
    #  Get list of users
    new_today = User.objects.filter(is_active=True, date_joined__gte=timezone.now() - datetime.timedelta(hours=24)).count()
    new_this_month = User.objects.filter(is_active=True, date_joined__gte=timezone.now() - datetime.timedelta(hours=24*30)).count()
    subscribers = User.objects.filter(is_active=True, profile__subscribed=True).count()
    return render(request, 'users/users.html', { #  Return users in a template
        'title': 'All Accounts',
        'users': User.objects.all(),
        'new_today': new_today,
        'new_this_month': new_this_month,
        'subscribers': subscribers
    })
Note that this code uses a test, we will need to declare this test in a tests.py file and import it. Editing users/tests.py, let's create the test.

def is_superuser_or_vendor(user):
    return user.profile.vendor or user.is_superuser
This is in conjunction with the users/users.html template, which looks something like this:
 
{% extends 'base.html' %}
{% load app_filters %}
{% block content %}
<h1>All Registered Visitors</h1>
<p>{{ new_today|nts|capitalize }} new today, {{ new_this_month|nts }} new this month, {{ subscribers|nts }} subscribers, {{ users.count|nts }} total.</p>
<hr style="color: red;">
{% for user in users %}
{% include 'users/_user.html' %}
<hr style="color: blue;">
{% endfor %}
{% endblock %}
 
Note that the template includes another template, users/_user.html. When using a template that has a subtemplate and not using extends, its a good idea to add an underscore (_) before the name of the file to extend, in order to distinguish templates. Note that this is a lot of jinja, you may not have all of these variables defined. But this is what my code looks like.
 
{% load app_filters %}
<div>
<img src="{{ user.profile.get_image_url }}" alt="@{{ user.profile.name }}'s profile photo" width="120" height="120" align="left" style="margin-top:5px; margin-right:10px; margin-bottom:10px; border-radius: 50%;"/>
    <div class="article-metadata">
      <p class="mr-2">@{{ user.username }} - {{ user.profile.name }} ({{ user.profile.preferred_name }})</p>
      <small class="text-muted">Last seen {{ user.profile.last_seen|date:"F d, Y" }} {{ user.profile.last_seen|time:"H:i" }}</small>
      <small class="text-muted">Joined on {{ user.profile.date_joined|date:"F d, Y" }} {{ user.profile.date_joined|time:"H:i" }}</small>
      <small>{{ user.email }}</small>
      {% if user.profile.phone_number %}<small><i class="bi bi-phone-fill"></i>{{ user.profile.phone_number }}</small>{% endif %}
      {% if user.verifications.last %}
      <small>'{{ user.verifications.last.full_name }}'</small>
      <small><i class="bi bi-123"></i> {{ user.verifications.last.document_number }}</small>
      <small><i class="bi bi-calendar-heart-fill"></i> {{ user.verifications.last.birthdate }}</small>
      <a href="{{ user|document_front }}" class="btn btn-sm btn-outline-primary" title="ID front"><i class="bi bi-person-badge-fill"></i> ID front</a>
      <a href="{{ user|document_back }}" class="btn btn-sm btn-outline-primary" title="ID back"><i class="bi bi-upc-scan"></i> ID back</a>
      {% endif %}
      <small># {{User.id}} </Small>
      <small>{% if user.profile.subscribed %}Subscribed{% else %}Not subscribed{% endif %}</small>
    </div>
    {%if not user.is_superuser %}
    <div style="float: right;">{% include 'users/toggle_active.html' %}</div>
    {% endif %}
    {% autoescape off %}    
    <p class="article-content">{{ user.bio }}</p>
    {% endautoescape %}
    <hr>
    <p>{% if user.profile.identity_verified %}Verified user.{% else %}Unverified user.{% endif %} Verifications: {{ user.verifications.count|nts }}</p>
 
We also need another subtemplate, toggle_active.html. This template should be a form that allows us to toggle whether a user is active.
 
<form style="display: inline-block;" action="{% url 'users:toggle-user-active' user.id %}" method="POST" id="publishForm">
<button class="btn btn-sm btn-outline-danger" type="submit">{% if user.is_active %}<i class="bi bi-eye-fill"></i>{% else %}<i class="bi bi-eye-slash-fill"></i>{% endif %}</button>
</form>
 
We will also need to add a view to toggle user activity, and appropriate URL patterns. While we are at it, let's add a view to delete a user in case we need that.

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
@login_required
@user_passes_test(is_superuser_or_vendor)
def toggle_user_active(request, pk):
    user = User.objects.get(id=pk)
    if request.method == 'POST':
        user.is_active = not user.is_active
        user.save()
    return HttpResponse('<i class="bi bi-eye-fill"></i>' if user.is_active else '<i class="bi bi-eye-slash-fill"></i>')


# Amounts
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.views.generic import DeleteView

class UserDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = User
    success_url = '/' #  The redirect on success URL
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        return context

    def test_func(self): #  Test if user is superuser and has permission to delete
        user = self.get_object()
        if self.request.user != user and self.request.user.is_superuser:
            return True
        return False
While this is practical when necessary, deleting a user shouldn't be necessary most of the time, we can just toggle the visibility of users who visit the site if we need to dismiss them. The URL patterns we added look like this. With nano, edit users/urls.py and add these lines:

nano users/urls.py
The lines should go in the list of paths in the user views, before the ending “]” but after the beginning “[“.

#  …
    path('user/<int:pk>/delete/', UserDeleteView.as_view(template_name='blog/user_confirm_delete.html'), name='delete-user'),
    path('user/<int:pk>/active/', views.toggle_user_active, name='toggle-user-active'),
#  …
Now, make sure to back up the site so you can download it on the web server we will continue working on. From the command line,

sudo backup
Now our site is backed up. So now we have a few more useful features. But what about the big picture here? This code still isn't accessible from the internet, we have no mail server yet, and we need to expand our app to include comprehensive verification process as well as smooth layouts to help us explore the site, along with secure protocols for authenticating privileged users. We will get to all this. The most important thing for now will just be getting this code online, which we can do with just a few lines of Bash on an Ubuntu server. You will need to rent a server for this though, unless you have a server at home and a business internet subscription that allows you to open ports. I personally run my website on an HP Z440 that is installed in my apartment, but it's usually much cheaper for basic needs to rent a Virtual Private Server (VPS). Keep in mind that the code we are running now is relatively thin, it will need to be maintained and improved before we are ready to use what we have to build a product. Make sure to be careful what you do with the internet, make sure if you deploy this site publicly to the web on a Linux server, you have a plan to block unwanted interactions with your website. This likely won't be a problem at first, but we will look into a variety of solutions to combat this, including machine learning, artificial intelligence and computer vision. When it does become a problem, look further in this text for a solution. In terms of renting a VPS, there are a lot of places you can go. Google Cloud has VPS servers, Ionos, Kamatera, Amazon AWS, and more providers offer cloud server solutions that will suit our needs. You'll need to click through their forms and select a plan to get started. You can go with a basic plan with any provider, but make sure the provider allows you to open port mail server ports to send email (this should be port 587 and port 25), some providers block these ports. So far I have had the best experience with Ionos and Kamatera, both of them will allow me to send unlimited email and their pricing is pretty cheap. You will connect to your new server over a protocol called SSH or secure shell, which allows you to remotely interface with the server exactly like your personal computer, from your personal computer. When you set up the server, the hosting provider is likely going to ask you to add an SSH key, or they will give you a username and password. The SSH key is how you will log in to the server from the command line to edit the code. Use the below ssh-keygen options to generate an ssh key.

ssh-keygen
Save the file and overwrite it if you need to, it's good to rotate your SSH keys if you haven't already. Now, you can use the following command to see your SSH key. You will want to copy it to your remote server so you can use it to authenticate.

cat ~/.ssh/id_rsa.pub
If you weren't able to see an SSH key when typing that command (a long string of digits and letters starting with “ssh-rsa AAA“), try generating an RSA key (they are more secure, so I advise to use them.) The following code will generate a 4096 bit RSA SSH key.

ssh-keygen -t rsa -b 4096
Create a VPS running Ubuntu, however you plan to do this. Once you have created a VPS by clicking through the forms on the providers website (kamatera.com, ionos.com or similar), you'll want to log in. To do this, use the ssh command with your IP address (the address that looks like XX.XX.XX.XX). You'll also need to be sensitive to the default username on the server we created, for example, ubuntu.

ssh ubuntu@XX.XX.XX.XX
You may be asked for a password, if you are asked for a password, enter it in. We won't use the default username, so let's start by creating a new user and adding an SSH key to their account. Let's start by adding a new sshd_config file, which tells the server how to use SSH.

nano sshd_config

#  This is the sshd server system-wide configuration file.  See
#  sshd_config(5) for more information.

#  This sshd was compiled with PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

#  The strategy used for options in the default sshd_config shipped with
#  OpenSSH is to specify options with their default value where
#  possible, but leave them commented.  Uncommented options override the
# Default Value.

# Port 22
# AddressFamily any
# List address 0.0.0.0
# Listen adress ::

# HostKey /etc/ssh/ssh_host_rsa_key
# HostKey /etc/ssh/ssh_host_ecdsa_key
# HostKey /etc/ssh/ssh_host_ed25519_key

#  Ciphers and keying
# Rekeylimit Default None

# Logging
# SyslogFacility AUTH
# Loglevel Info

#  Authentication:

# Logringracetime 2m
# PermitRootLogin prohibit-password
# StrictModes yes
# MaxAuthTries 6
# Maxessions 10

PubkeyAuthentication yes

#  Expect .ssh/authorized_keys2 to be disregarded by default in future.
AuthorizedKeysFile	.ssh/authorized_keys .ssh/authorized_keys2

# AuthorizedPrincipalsFile none

# AuthorizedKeysCommand none
# AuthorizedKeysCommandUser nobody

#  For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
# HostbasedAuthentication no
#  Change to yes if you don't trust ~/.ssh/known_hosts for
#  HostbasedAuthentication
# IgnoreUserKnownHosts no
#  Don't read the user's ~/.rhosts and ~/.shosts files
# IgnoreRhosts yes

#  To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no
# PermitEmptyPasswords no

#  Change to yes to enable challenge-response passwords (beware issues with
#  some PAM modules and threads)
KbdInteractiveAuthentication no

#  Kerberos options
# KerberosAuthentication no
# KerberosOrLocalPasswd yes
# KerberosTicketCleanup yes
# Kerberoscotted rod no

#  GSSAPI options
# GSSAPIAuthentication no
# GSSAPICleanupCredentials yes
# GSSAPIStrictAcceptorCheck yes
# GSSAPIKeyExchange no

#  Set this to 'yes' to enable PAM authentication, account processing,
#  and session processing. If this is enabled, PAM authentication will
#  be allowed through the KbdInteractiveAuthentication and
#  PasswordAuthentication.  Depending on your PAM configuration,
#  PAM authentication via KbdInteractiveAuthentication may bypass
#  the setting of "PermitRootLogin without-password".
#  If you just want the PAM account and session checks to run without
#  PAM authentication, then enable this but set PasswordAuthentication
#  and KbdInteractiveAuthentication to 'no'.
UsePAM yes

# AllowAgentForwarding yes
# AllowTcpForwarding yes
# GatewayPorts no
X11Forwarding yes
# X11DisplayOffset 10
# X11UseLocalhost yes
# PermitTTY yes
PrintMotd no
# PrintLastLog yes
# TCPKeepAlive yes
# Permittuenvironment in
# Compression delayed
# Clientalive interval 0
# ClientAliveCountMax 3
# Usedns in
# Pidfile /run/sshd.pid
# MaxStartups 10:30:100
# PEMITTUNL NO
# ChrootDirectory none
# Version addendum None

#  no default banner path
Banner /etc/banner

#  Allow client to pass locale environment variables
AcceptEnv LANG LC_*

#  override default of no subsystems
Subsystem	sftp	/usr/lib/openssh/sftp-server

#  Example of overriding settings on a per-user basis
# Match User anoncvs
# 	X11Forwarding no
# 	AllowTcpForwarding no
# Allowetty in
# 	ForceCommand cvs server
PermitRootLogin no
Remember, ctrl+x and Y to save the file. Next, let's write a basic script called initialize (all in the default home directory of our user).

nano initialize
Add these lines to the file, replacing with your SSH key you found using cat. (.ssh/id_rsa.pub)

# ! / bin / Bash
sudo apt install -y nano git openssh-server
sudo cp sshd_config /etc/ssh/sshd_config
sudo service ssh restart
sudo service sshd restart
echo "/root/.ssh/id_rsa" | sudo su root -c "ssh-keygen -t rsa -N ''"
echo "root ssh key:"
sudo su root -c "cat /root/.ssh/id_rsa.pub"
sudo adduser --disabled-password --gecos "" team
sudo passwd -d team
sudo usermod -aG sudo team
echo "/home/team/.ssh/id_rsa" | su team -c "ssh-keygen -t rsa -N ''"
cat /home/team/.ssh/id_rsa.pub >> /home/team/.ssh/authorized_keys
echo '<key here>' >> /home/team/.ssh/authorized_keys
echo "team ssh key:"
cat /home/team/.ssh/id_rsa.pub
To walk you through this file, let's start line by line. The first line tells the compiler that this is a bash script. Then we are installing dependencies, copying sshd_config to the correct directory, restarting ssh, generating ssh keys for root, adding the user 'team' (you can pick a name you like for this, use the adduser command with their name and disabled password for now). We also add team to the sudo group, generate their ssh key, add our key to authorized keys and theirs as well, and print their key. This new user will be how we log into the site. In a new terminal, go ahead and open up the server again.

ssh team@XX.XX.XX.XX
You shouldn't need a password this time, being as you have an SSH key. We have also disabled login with password to keep the site more secure. Now, this server starts up completely blank with no information on it. Let's start by cloning our project from git so we can download and run it on the remote machine. On the remote server connected over SSH, first print your SSH key:

cat ~/.ssh/id_rsa.pub
Next, paste this key into the git settings like we did before to set up our git repository. We may now clone our project directly to the server. Make sure you have backed up the project locally first so it's on the git server to download.

git clone git://github.com/you/yourproject.git
Perfect. Now all of the files are here. We can see them with ls

ls
Now, let's begin to set up the server. First, copy your project directory into a simple, memorable name we will use for the project.

cp -r yourproject whatyoucalledit
Where “whatyoucalledit” is the new name of your project. Next, we will need to build a basic utility to set up the server. We will save this utility and use it in the future. To build this utility, let's create a user binary to define how we edit a script. Using bash, edit /usr/bin/ascript

sudo nano /usr/bin/ascript
Make sure to use sudo there so you have permissions to edit the file. In the file, add these lines:

# ! / bin / Bash
if [ ! -f /usr/bin/$1 ]; then
    sudo touch /usr/bin/$1
    echo "# ! / bin / Bash ">> / USR / BIN / $ 1
    sudo chmod a+x /usr/bin/$1
    sudo nano /usr/bin/$1
    echo $1 | sudo tee -a /etc/ascripts
else
    sudo chmod a+x /usr/bin/$1
    sudo nano /usr/bin/$1
fi
Remember this script takes an argument, the script name, as $1. First it checks if the file exists, or otherwise creates it, adds the first line to declare the script is bash, changes its permissions, edits it, and adds its name to /etc/ascripts which lets us store the names of the scripts we are creating. If the file already exists, simply change permissions and edit it. Save the file, and next we will change it's permissions. As long as we use this script, we won't have to do that again.

sudo chmod a+x /usr/bin/ascript
Perfect. Now let's create a script called setup. First, not to overwhelm you, but take a look at what my setup script looks like. We will walk through what this script should look like in your project, you won't need everything in my script to start with.

# ! / bin / Bash
SECONDS=0
PYTHON_VERSION=3.12
echo "femmebabe installer initialized."
#  sudo chmod a+x scripts/usersetup
# ./scripts/usersetup
# SSH-Keyen
#  Project directory
DIR="/home/team/femmebabe"
USER="team"
#  Log commands
echo "Logging commands"
sudo cp log/commands.log /var/log/commands.log
sudo chmod -R a+w /var/log
sudo chown -R :syslog /var/log
echo $'alias venv="source /home/team/femmebabe/venv/bin/activate"' | sudo tee -a /home/team/.profile
echo $'PROMPT_COMMAND=\'RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"\'' | sudo tee -a /etc/bashrc
echo $'PROMPT_COMMAND=\'RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"\'' | sudo tee -a "/home/team/.bashrc"
echo $'PROMPT_COMMAND=\'RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"\'' | sudo tee -a /root/.bashrc
echo "source /etc/bashrc" | sudo tee -a /home/team/.profile
echo "/var/log/commands.log" | sudo tee -a /etc/logrotate.d/syslog
echo "local6.*    /var/log/commands.log" | sudo tee -a "/etc/rsyslog.d/bash.conf"
sudo service rsyslog restart
# Nano config
echo "set tabsize 4" >> .nanorc
echo "set tabstospaces" >> .nanorc
# Git config
echo "Git configuration"
sudo git config --global user.email "jasper.camber.holton@gmail.com" && sudo git config --global user.name "Jasper Holton"
git config --global user.email "jasper.camber.holton@gmail.com"
git config --global user.name "Jasper Holton"
git config --global --add safe.directory $"$DIR"
sudo ssh-keyscan -t rsa gitlab.com | sudo tee -a /root/.ssh/known_hosts
sudo ssh-keyscan -t rsa github.com | sudo tee -a /root/.ssh/known_hosts
echo "Mounting setup"
sudo mount -o remount,size=16G,exec /tmp
#  Update and install
echo "Update and install packages"
sudo apt update && sudo NEEDRESTART_MODE=a apt upgrade -y
sudo apt purge postgresql-client-14 postgresql-client-common postgresql-common postgresql-contrib postgresql -y
echo "postfix postfix/mailname string femmebabe.com" | sudo debconf-set-selections
echo "postfix postfix/main_mailer_type string 'Internet Site'" | sudo debconf-set-selections
sudo NEEDRESTART_MODE=a DEBIAN_FRONTEND=noninteractive apt install -y postfix
sudo NEEDRESTART_MODE=a apt install -y rkhunter clamav-daemon libx264-dev ffmpeg libapache2-mod-wsgi-py3 apache2 cmake python-is-python3 python3-venv python3-pip python3-django expect tesseract-ocr openjdk-8-jdk redis-server libopencv-dev python3-opencv python3-dev libsasl2-dev opendkim opendkim-tools dovecot-core dovecot-pop3d dovecot-imapd auditd procmail libpq-dev postgresql postgresql-contrib libheif-dev snapd git software-properties-common certbot python3-certbot-apache
echo "-a exit,always -F arch=b64 -F euid=0 -S execve" | sudo tee -a /etc/audit/audit.rules
echo "-a exit,always -F arch=b32 -F euid=0 -S execve" | sudo tee -a /etc/audit/audit.rules
# Enable Clamav Antivirus
echo "Starting antivirus"
sudo systemctl enable clamav-daemon
sudo systemctl start clamav-daemon
# Set hostname
echo "127.0.0.1 femmebabe" | sudo tee -a /etc/hosts
sudo hostnamectl set-hostname localhost
# Setup postgres
echo "Postgres setup"
sudo -u postgres psql -U postgres -c "DROP DATABASE database;"
sudo -u postgres psql -U postgres -c "CREATE DATABASE database;"
sudo -u postgres psql -U postgres -c "CREATE USER django WITH PASSWORD 'password';"
sudo -u postgres psql -U postgres -c "ALTER ROLE django SET client_encoding TO 'utf8';"
sudo -u postgres psql -U postgres -c "ALTER ROLE django SET default_transaction_isolation TO 'read committed';"
sudo -u postgres psql -U postgres -c "ALTER ROLE django SET timezone TO 'UTC';"
sudo -u postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE database TO django;"
# Setup Backup Database
echo "Building database from backup, this may take a while."
cat db.json.?? > db.json
echo "Configuring firewall"
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow 22
sudo ufw allow http
sudo ufw allow https
sudo ufw allow 'Postfix'
sudo ufw allow 'Postfix SMTPS'
sudo ufw allow 'Postfix Submission'
sudo ufw allow 'Dovecot POP3'
sudo ufw allow 'Dovecot Secure POP3'
sudo ufw allow 110/tcp
sudo ufw allow 25/tcp
echo "y" | sudo ufw enable
# Disabled iPatables
echo "Configuring firewall"
sudo iptables -P INPUT ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -F
sudo iptables-save
# Install Bitdefender
cd $DIR
echo "Runnning BitDefender antivirus installer"
wget https://cloud.gravityzone.bitdefender.com/Packages/NIX/0/7aTSsy/setup_downloader.tar
mkdir bitdefender
tar -xf setup_downloader.tar -C bitdefender
sudo rm setup_downloader.tar
sed -i -e 's/{LOGINPASSWD/z&A*3BPd_qBGUMs/g' bitdefender/installer
sudo chmod a+x bitdefender/installer
sudo ./bitdefender/installer
# Setup postfix
cd $DIR
echo "Mail services configuration"
sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.backup
sudo cp config/etc_postfix_main.cf /etc/postfix/main.cf
sudo cp config/etc_postfix_master.cf /etc/postfix/master.cf
sudo cp config/etc_default_opendkim /etc/default/opendkim
sudo cp config/etc_dovecot_conf.d_10-auth.conf /etc/dovecot/conf.d/10-auth.conf
sudo cp config/etc_dovecot_conf.d_10-master.conf /etc/dovecot/conf.d/10-master.conf
sudo cp config/etc_dovecot_dovecot.conf /etc/dovecot/dovecot.conf
sudo cp config/etc_dovecot_passwd /etc/dovecot/passwd
sudo cp config/etc_opendkim.conf /etc/opendkim.conf
sudo cp config/etc_default_opendkim /etc/default/opendkim
sudo adduser postfix opendkim
sudo mkdir /etc/opendkim
sudo mkdir /etc/opendkim/keys
sudo mkdir /etc/opendkim/keys/femmebabe.com
sudo mkdir /var/spool/postfix/opendkim
sudo echo "*@femmebabe.com     sendonly._domainkey.femmebabe.com" | sudo tee -a /etc/opendkim/signing.table
sudo echo "sendonly._domainkey.femmebabe.com    femmebabe.com:sendonly:/etc/opendkim/keys/femmebabe.com/sendonly.private" | sudo tee -a /etc/opendkim/key.table
sudo echo "127.0.0.1" | sudo tee -a /etc/opendkim/trusted.hosts
sudo echo "localhost" | sudo tee -a /etc/opendkim/trusted.hosts
sudo echo "" | sudo tee -a /etc/opendkim/trusted.hosts
sudo echo "*.femmebabe.com" | sudo tee -a /etc/opendkim/trusted.hosts
sudo chown -R opendkim:opendkim /etc/opendkim
sudo opendkim-genkey -b 2048 -d femmebabe.com -D /etc/opendkim/keys/femmebabe.com -s sendonly -v
sudo chmod go-rw /etc/opendkim/keys
sudo chown opendkim:opendkim /etc/opendkim/keys/femmebabe.com/sendonly.private
sudo chown opendkim:postfix /var/spool/postfix/opendkim
cd $DIR
sudo cp mailbox/* /var/mail/
sudo chown :users /var/mail/*
sudo chmod -R a+rwx /var/mail/*
sudo systemctl restart opendkim postfix dovecot
#  Create dirs
cd $DIR
mkdir media/audio
mkdir media/audio/fingerprints
mkdir media/security
mkdir media/secure
mkdir media/secure/media
mkdir media/secure/video
mkdir media/secure/profile
mkdir media/secure/face
mkdir media/images
mkdir media/live
mkdir media/live/files
mkdir media/live/stills
mkdir media/files
mkdir temp
mkdir temp/data
mkdir temp/gfpgan
mkdir mail/inbox
mkdir mailbox
# Setup virtuealenv
cd $DIR
echo "Creating virtual environment"
python -m venv venv
source venv/bin/activate
# Get and build Dependencies
echo "Getting and building dependencies, this may take a whike"
cd $DIR
git clone https://github.com/sukhitashvili/violence-detection.git
cp config/vd-requirements.txt violence-detection/requirements.txt
cp config/vd-model.py violence-detection/model.py
cd violence-detection
pip3 install -r requirements.txt
cd $DIR
wget https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth -P experiments/pretrained_models
git clone https://github.com/TencentARC/GFPGAN.git
git clone https://github.com/davisking/dlib.git
cd dlib
mkdir build; cd build; cmake ..; cmake --build .
cd ..
source venv/bin/activate
python setup.py install
cd $DIR
source venv/bin/activate
cd $DIR/GFPGAN/
echo "Installing python dependencies"
pip install basicsr
pip install facexlib
pip install -r requirements.txt
python setup.py develop
pip install realesrgan
cd $DIR
sudo chown -R team:users gfpgan
echo "Installing ta-lib"
wget https://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
tar xvzf ta-lib-0.4.0-src.tar.gz
sudo rm ta-lib-*
cd ta-lib
sudo ./configure
sudo make
sudo make install
#  Set firewall rules
cd $DIR
# Install Pypi Dependencies
echo "Installing remaining python dependencies (this may take a while)"
sudo systemctl mask tmp.mount
cd $DIR
source venv/bin/activate
pip3 install -U "celery[redis]"
pip3 install -r requirements.txt --use-deprecated=legacy-resolver --use-pep517
pip3 install --upgrade opencv-python # ==4.5.4.60
pip3 install --upgrade opencv-contrib-python # ==4.5.4.60
# pip install opencv-python==4.5.5.64
# pip install opencv-contrib-python==4.5.5.64
pip3 install --upgrade opencv-python-headless
pip3 uninstall channels
pip3 uninstall daphne
pip3 install channels["daphne"]
pip3 install Pillow==9.5.0
pip3 install librosa
pip3 install -U 'Twisted[tls,http2]'
pip3 install --upgrade certifi requests urllib3 numpy oauthlib twisted pyjwt sqlparse cryptography astral webauthn docbarcodes pdf417 deepface --no-cache-dir
pip3 install tensorflow==2.15.1
#  Install certbot
echo "Installing certificates"
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo snap install redis
sudo systemctl enable apache2
sudo systemctl start apache2
# Run Certbot
sudo certbot --apache --non-interactive --agree-tos --domains femmebabe.com --email jasper.camber.holton@gmail.com
#  Reload mail server
sudo systemctl restart opendkim postfix dovecot
#  Copy certs
# sudo cp /etc/letsencrypt/live/femmebabe.com/privkey.pem privkey.pem
# SUDO CP /ETC/LETSENCRYPT/LIVE/FEMMEBABE.com/cert.PEM CERT.PEM
# Patch
cp scripts/content.py $"/home/team/femmebabe/venv/lib/python${PYTHON_VERSION}/site-packages/pyxb/binding/content.py"
cp scripts/pwa_webpush_forms.py $"/home/team/femmebabe/venv/lib/python${PYTHON_VERSION}/site-packages/pwa_webpush/forms.py"
cp scripts/webauth_views.py $"/home/team/femmebabe/venv/lib/python${PYTHON_VERSION}/site-packages/webauth/views.py"
cp scripts/json.py $"venv/lib/python${PYTHON_VERSION}/site-packages/django/core/serializers/json.py"
# Set User Settings
sudo gpasswd -a www-data users
# Set permissions
echo "Setting permissions"
sudo chown -R team:users cache/
sudo chmod a+rwx -R cache/
# sudo chown -R team:users /var/run/
# sudo chown root:root /run/sudo/ts -R
sudo chown -R redis:redis /var/lib/redis
sudo chown -R redis:redis /var/log/redis
sudo chmod -R u+rwX,g+rwX,u+rx /var/log/redis
sudo chmod +r /etc/redis/redis.conf
sudo chown -R team:users /var/log/
sudo chown -R :users .././
sudo chmod -R g+rwX ./
sudo chmod -R g+rX .././
sudo chmod -R g-rwX ../.ssh
sudo chmod 774 ./
# sudo chmod 664 db.sqlite3
# sudo chown www-data:users db.sqlite3
sudo chown -R www-data:www-data media/
sudo chown www-data:users ./
sudo chown -R team:users media/
sudo chown -R team:users ./
sudo chown -R team:users ./gfpgan/
sudo chown -R team:users ./temp/
sudo chmod a+r team /var/mail/$USER
#  Copy config and set permissions
echo "Configuring remaining services"
sudo cp config/apis.json /etc/apis.json
sudo cp config/config.json /etc/config.json
sudo cp config/femmebabe-le-ssl.conf /etc/apache2/sites-available/femmebabe-le-ssl.conf
sudo cp config/etc_dovecot_passwd /etc/dovecot/passwd
sudo cp config/etc_init.d_celery /etc/init.d/celery
sudo cp config/etc_init.d_celerybeat /etc/init.d/celerybeat
sudo cp config/etc_default_celerybeat /etc/default/celerybeat
sudo cp config/etc_default_celery /etc/default/celery
sudo cp config/etc_systemd_system_daphne.service /etc/systemd/system/daphne.service
sudo cp config/etc_systemd_system_celery.service /etc/systemd/system/celery.service
sudo cp config/etc_systemd_system_celerybeat.service /etc/systemd/system/celerybeat.service
sudo chmod a+x /etc/init.d/celery
sudo chmod a+x /etc/init.d/celerybeat
# Database setup
echo "Running migrations, this should be quick"
python manage.py makemigrations
python manage.py migrate --run-syncdb
echo "Loading data, this may take a while"
python manage.py loaddata db.json
echo "Setup crontab/sudoers configuration"
sudo crontab -l -u root | cat - config/crontab | sudo crontab -u root -
sudo sh -c "cat config/sudoers >> /etc/sudoers"
#  Inject pam config and remove faulty ssh config
# SUDO SED -I '' -and $ d '/etc/pam.d/sshd
# SUDO SED -I '' -and $ d ' /etc /profile
echo "session required pam_exec.so seteuid /home/team/femmebabe/pam.sh" | sudo tee -a /etc/pam.d/sshd
echo "session required pam_exec.so seteuid /home/team/femmebabe/logout.sh" | sudo tee -a /etc/pam.d/sshd
sudo chmod a+x pam.sh
sudo rm /etc/ssh/sshd_config.d/50-cloud-init.conf
#  Copy bin scripts and set permissions
echo "Copying scripts"
sudo cp scripts/reload /usr/bin/
sudo cp scripts/check /usr/bin/
sudo cp scripts/enagpu /usr/bin/
sudo cp scripts/disgpu /usr/bin/
sudo cp scripts/activate /usr/bin/
sudo cp scripts/backup /usr/bin/
sudo cp scripts/ascript /usr/bin/
sudo cp scripts/setup /usr/bin/
sudo cp scripts/addsetup /usr/bin/
sudo cp scripts/watchlogs /usr/bin/
sudo cp scripts/logs /usr/bin/
sudo cp scripts/cmds /usr/bin/
sudo cp scripts/setup /usr/bin/
sudo cp scripts/pushweb /usr/bin/
sudo cp scripts/purgecache /usr/bin/
sudo cp config/banner /etc/banner
cd /usr/bin/
sudo chmod a+x activate
sudo chmod a+x backup
sudo chmod a+x ascript
#  Reload and enable services
echo "Enabling services"
sudo systemctl daemon-reload
sudo systemctl enable daphne.service
sudo systemctl enable celery.service
sudo systemctl enable celerybeat.service
sudo systemctl enable clamav-daemon
sudo systemctl start daphne.service
sudo systemctl start celery.service
sudo systemctl start celerybeat.service
sudo systemctl start clamav-daemon
# Enable Apache Modules
echo "Enabling apache2"
sudo a2enmod rewrite
sudo a2enmod wsgi
sudo a2enmod headers
sudo a2enmod ssl
sudo a2enmod proxy
sudo a2enmod proxy_balancer
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
# sudo a2dismod mpm_event
# sudo a2dismod mpm_worker
# sudo a2enMod MPM_PreFork
# Disable Default Site
sudo a2dissite 000-default
sudo a2dissite 000-default-le-ssl
# Enable for site
sudo a2ensite femmebabe-le-ssl
#  Reload daemon and restart apache, postfix and opendkim
sudo systemctl daemon-reload
sudo systemctl restart apache2
sudo systemctl restart opendkim postfix
sudo systemctl start daphne
# Set permissions
sudo chown -R :www-data /var/www/
sudo chown -R :www-data /var/www/.deepface
#  Swap configuration
echo "Allocating swap, this may take a while"
sudo swapoff /swapfile
sudo rm /swapfile
sudo fallocate -l 8G /swapfile
sudo dd if=/dev/zero of=/swapfile bs=1024 count=8388608
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo "/swapfile swap swap defaults 0 0" | sudo tee -a /etc/fstab
sudo swapon --show
#  Init caption engine
echo "Initializing routine caption"
/home/team/femmebabe/venv/bin/python /home/team/femmebabe/routine_caption.py
/home/team/femmebabe/venv/bin/python /home/team/femmebabe/setup_mail.py
# Setup git
echo "Setting up git"
cd $DIR
sudo rm -r .git
git init --initial-branch=main
echo "Setting user password"
sudo usermod --password $(echo team | openssl passwd -1 -stdin) team
#  Show ipv6 and opendkim for domain configuration
echo "COPY the below information to domain configuration."
hostname -I
ip a | grep inet
ip -6 addr | grep "scope link"
sudo cat /etc/opendkim/keys/femmebabe.com/sendonly.txt | tr -d '\n' | sed 's/\s//g' | sed 's/""//g' | awk -F'[)(]' '{print $2}'
#  Setup completed
echo "Setup completed in"
wc -l scripts/setup
echo "lines of code."
echo "Total time:"
duration=$SECONDS
echo "$((duration / 60)) minutes and $((duration % 60)) seconds elapsed."
echo "TODO:"
echo "- COPY above IPv6 address to domain DNS configuration"
echo "- COPY domain key to domain DNS configuration"
echo "- ADD new git repository with git remote add originlab <repo>."
echo "- OPEN port 25"
echo "- INSTALL antivirus as per reccomendations"
echo "- TEST"
echo "If neccesary,"
echo "- DEBUG"
echo "- FIX setup and backup scripts"
echo "- Fix server"
echo ""
echo "Thank you for using the femmebabe installer. Have a great day!"
echo "Goodbye."
That's a lot of setup! In short, this code logs commands, configures nano and git, copies over files, downloads and installs ubuntu apt packages, python dependencies, configures postfix, configures postgresql (the database server) and loads the database, configures ufw (an uncomplicated firewall), disables iptables, downloads an antivirus, makes directories, clones dependencies, installs certificates and sets up the server, installs configuration, starts and enables the sever, allocates swap, sets permissions, and prints the IP, IPV6 address and OpenDKIM key. Fairly simple, but it looks like a lot of code. We won't need a lot of this because we don't have the dependencies, we aren't using celery, celerybeat or daphne, but we will install some of them anyway to get started. Notice that this code has a domain declared several times. We will also need to purchase a domain name (which is a small yearly fee). I recommend squarespace for purchasing a domain, their layout is intuitive and easy to use. You can buy any domain of your choice, but I am using the domain femmebabe.com in this example. Once you have bought a domain, head to the squarespace DNS configuration panel and add an A record pointing your domain to the server by IP address. It should look like this: @ A XX.XX.XX.XX With the @ operator as the host, meaning all subdomains under this domain and the root domain will all redirect to the server. There are more records to declare, but we can move on to these once we are ready to send mail. Keep in mind, it may take several days before you are able to successfully send mail from the server. The DNS records we are setting will take time to propagate. Anyway, the only record we need to start is an A record. So now we can fill in the below script according to our project and run it. Let's start with a smaller setup script to just install what we need for a basic progress. We won't use so many dependencies or postgresql yet, we'll just start up a basic HTTP server and worry about certifying it when that's done. Remember, to get an HTTPS certificate and run the server securely, we will need to buy a domain along with rent a server. For now, replace “team” in this file with the name of your user, “DIR” with the directory of your project, and supply your email and domain in the < > tags. Additionally, before we run this code, we need to change the settings to the firewall the hosting provider supports, if any. Usually this is in the 'Networks' tab of your hosting provider, or if you are self hosting, its in the 'port forwarding' section of your router. You will also want to set up a static IP through your router with the address of your server machine, if you are using self hosting. You will need to open the following ports for read/write access. 22 (ssh) 25 (mail) 587 (mail) 110 (mail client) 80 (http) 443 (https)

# ! / bin / Bash
SECONDS=0
PYTHON_VERSION=3.12
echo "femmebabe installer initialized."
DIR="/home/team/<yourproject>"
USER="team"
#  Log commands
echo "Logging commands"
sudo cp log/commands.log /var/log/commands.log
sudo chmod -R a+w /var/log
sudo chown -R :syslog /var/log
echo $'alias venv="source /home/team/femmebabe/venv/bin/activate"' | sudo tee -a /home/team/.profile
echo $'PROMPT_COMMAND=\'RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"\'' | sudo tee -a /etc/bashrc
echo $'PROMPT_COMMAND=\'RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"\'' | sudo tee -a "/home/team/.bashrc"
echo $'PROMPT_COMMAND=\'RETRN_VAL=$?;logger -p local6.debug "$(whoami) [$$]: $(history 1 | sed "s/^[ ]*[0-9]\+[ ]*//" )"\'' | sudo tee -a /root/.bashrc
echo "source /etc/bashrc" | sudo tee -a /home/team/.profile
echo "/var/log/commands.log" | sudo tee -a /etc/logrotate.d/syslog
echo "local6.*    /var/log/commands.log" | sudo tee -a "/etc/rsyslog.d/bash.conf"
sudo service rsyslog restart
# Nano config
echo "set tabsize 4" >> .nanorc
echo "set tabstospaces" >> .nanorc
# Git config
echo "Git configuration"
sudo git config --global user.email "<youremail>@gmail.com" && sudo git config --global user.name "<yourname>"
git config --global --add safe.directory $"$DIR"
sudo ssh-keyscan -t rsa gitlab.com | sudo tee -a /root/.ssh/known_hosts
sudo ssh-keyscan -t rsa github.com | sudo tee -a /root/.ssh/known_hosts
#  Update and install
echo "Update and install packages"
sudo apt update && sudo NEEDRESTART_MODE=a apt upgrade -y
sudo apt purge postgresql-client-14 postgresql-client-common postgresql-common postgresql-contrib postgresql -y
echo "postfix postfix/mailname string femmebabe.com" | sudo debconf-set-selections
echo "postfix postfix/main_mailer_type string 'Internet Site'" | sudo debconf-set-selections
sudo NEEDRESTART_MODE=a DEBIAN_FRONTEND=noninteractive apt install -y postfix
sudo NEEDRESTART_MODE=a apt install -y rkhunter clamav-daemon libx264-dev ffmpeg libapache2-mod-wsgi-py3 apache2 cmake python-is-python3 python3-venv python3-pip python3-django expect tesseract-ocr openjdk-8-jdk redis-server libopencv-dev python3-opencv python3-dev libsasl2-dev opendkim opendkim-tools dovecot-core dovecot-pop3d dovecot-imapd auditd procmail libpq-dev postgresql postgresql-contrib libheif-dev snapd git software-properties-common certbot python3-certbot-apache
# Enable Clamav antivirus
echo "Starting antivirus"
sudo systemctl enable clamav-daemon
sudo systemctl start clamav-daemon
# Set hostname
echo "127.0.0.1 femmebabe" | sudo tee -a /etc/hosts
sudo hostnamectl set-hostname femmebabe
# Setup Backup Database
echo "Building database from backup, this may take a while."
cat db.json.?? > db.json
echo "Configuring firewall"
sudo ufw default allow outgoing
sudo ufw default deny incoming
sudo ufw allow 22
sudo ufw allow http
sudo ufw allow https
sudo ufw allow 'Postfix'
sudo ufw allow 'Postfix SMTPS'
sudo ufw allow 'Postfix Submission'
sudo ufw allow 'Dovecot POP3'
sudo ufw allow 'Dovecot Secure POP3'
sudo ufw allow 110/tcp
sudo ufw allow 25/tcp
echo "y" | sudo ufw enable
# Disable IPTables
echo "Configuring firewall"
sudo iptables -P INPUT ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -F
sudo iptables-save
# Setup Virtuenv
cd $DIR
echo "Creating virtual environment"
python -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
#  Install certbot
echo "Installing certificates"
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo snap install redis
sudo systemctl enable apache2
sudo systemctl start apache2
# Run Certbot
sudo certbot --apache --non-interactive --agree-tos --domains femmebabe.com --email <youremail>@gmail.com
# Set User Settings
sudo gpasswd -a www-data users
# Set permissions
echo "Setting permissions"
sudo chown -R team:users cache/
sudo chmod a+rwx -R cache/
# sudo chown -R team:users /var/run/
# sudo chown root:root /run/sudo/ts -R
sudo chown -R redis:redis /var/lib/redis
sudo chown -R redis:redis /var/log/redis
sudo chmod -R u+rwX,g+rwX,u+rx /var/log/redis
sudo chmod +r /etc/redis/redis.conf
sudo chown -R team:users /var/log/
sudo chown -R :users .././
sudo chmod -R g+rwX ./
sudo chmod -R g+rX .././
sudo chmod -R g-rwX ../.ssh
sudo chmod 774 ./
sudo chown -R www-data:www-data media/
sudo chown www-data:users ./
sudo chown -R team:users media/
sudo chown -R team:users ./
#  Reload and enable services
echo "Enabling services"
sudo systemctl daemon-reload
sudo systemctl enable clamav-daemon
sudo systemctl start clamav-daemon
#  Enable apache modules
echo "Enabling apache2"
sudo a2enmod rewrite
sudo a2enmod wsgi
sudo a2enmod headers
sudo a2enmod ssl
sudo a2enmod proxy
sudo a2enmod proxy_balancer
sudo a2enmod proxy_http
sudo a2enmod proxy_wstunnel
#  Reload daemon and restart apache, postfix and opendkim
sudo systemctl daemon-reload
sudo systemctl restart apache2
sudo systemctl restart opendkim postfix
#  Show ipv6 and opendkim for domain configuration
echo "COPY the below information to domain configuration."
hostname -I
ip a | grep inet
ip -6 addr | grep "scope link"
Before running this code, make sure the domain you have purchased is connected to the server. To do this, open a terminal on your local machine, and run this command with your domain:

ping femmebabe.com #  insert your domain here, after ping
If all looks well and the server is sending responses, we are ready to run the script and install packages as well as start, enable and certify our Apache server. This isn't all the setup needed to configure Postfix, we will look at that setup more later. For now, run this setup code and it should take a few minutes to install and certify your server. Once again, make sure to replace name, email and domain name in the script according to the name you purchased. Now that the server is provisioned, you can go to the URL in any web browser and check to make sure the server is running HTTPS. If it's not, try waiting a little while for the DNS records to catch up and then run the following command to retry certbot certification:

sudo certbot --apache --non-interactive --agree-tos --domains <domain>.com --email <youremail>@gmail.com
As long as you have configured everything correctly, you should be able to access apache's default page just to know your code is working and displaying a live webpage. Next, let's edit the settings.py to change our default debug mode to production. We'll also configure the domain in the settings, as well as internal IPs.

nano yourproject/settings.py
In the settings, change/add these lines.

DEBUG = False

# Site config
SITE_NAME = 'Femme Babe'
PROTOCOL = 'https'
DOMAIN = 'femmebabe.com'
SITE_ID = 1
BASE_URL = PROTOCOL + '://' + DOMAIN
ALLOWED_HOSTS = [DOMAIN]

INTERNAL_IPS = [
    'XX.XX.XX.XX',
]
Now, we will need to configure Apache2. Let's edit the config file we will deploy with this line:

sudo nano /etc/apache2/sites-available/femmebabe-le-ssl.conf
This config file should have our domain name in it, and the name of the user and project. I'm using the domain name femmebabe.com, the username team, and the project name femmebabe.

ServerSignature Off
ServerTokens Prod
<IfModule mod_ssl.c>
<VirtualHost *:80> 
	Redirect permanent / https://femmebabe.com/
</VirtualHost>
<VirtualHost *:443>
	ServerName femmebabe.com
	ServerAdmin team@femmebabe.com
	DocumentRoot /var/www/html

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined
	
	Alias /static /home/team/femmebabe/static
	<Directory /home/team/femmebabe/static>
		Require all granted
	</Directory>

Alias /media/icons /home/team/femmebabe/media/
<Directory /home/team/femmebabe/media>
Require all granted
</Directory>

	<Directory /home/team/femmebabe/femmebabe>
		<Files wsgi.py>
			Require all granted
		</Files>
	</Directory>

	WSGIScriptAlias / /home/team/femmebabe/femmebabe/wsgi.py
	WSGIDaemonProcess femmebabe python-path=/home/team/femmebabe/ python-home=/home/team/femmebabe/venv header-buffer-size=100000000000 user=team
	WSGIProcessGroup femmebabe
	WSGIApplicationGroup %{GLOBAL}
	
	<Directory /home/team/femmebabe/static>
                Options Indexes FollowSymLinks
                AllowOverride All
	</Directory>

	<IfModule mod_rewrite.c>
		RewriteEngine on
		RewriteCond %{REQUEST_URI} \.(css|webp|webm|gif|png|mp3|wav|jpeg|jpg|svg|webp)$ [NC]
		RewriteCond %{HTTP_REFERER} !^https://femmebabe.com/media/.*$ [NC]
		RewriteRule ^(.+?)/$ /media/$1 [F,L]
	</IfModule>

	Include /etc/letsencrypt/options-ssl-apache.conf
	SSLCertificateFile /etc/letsencrypt/live/femmebabe.com/fullchain.pem
	SSLCertificateKeyFile /etc/letsencrypt/live/femmebabe.com/privkey.pem

	Header set X-Frame-Options: "SAMEORIGIN"
	Header set Access-Control-Allow-Origin "https://femmebabe.com"

	TimeOut 60000
	LimitRequestBody 0

	<FilesMatch ".(ico|pdf|flv|jpg|jpeg|png|gif|webp|JPG|JPEG|wav|mp3|mp4|public|js|css|swf|webp|svg)$">
		Header set Cache-Control "max-age=30, public"
	</FilesMatch>
</VirtualHost>
</IfModule>
<IfModule mod_ssl.c>
<VirtualHost *:80>
	ServerName femmebabe.com
	ServerAdmin team@femmebabe.com
	DocumentRoot /var/www/html

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined

	RewriteEngine on
	RewriteCond %{SERVER_NAME} =femmebabe.com
	RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
</IfModule>
Make sure to replace the name of the project, directories, and domain in this example code when configuring your server. Now, we will need to disable the default site. This can be done using bash.

sudo a2dissite 000-default-le-ssl
sudo a2dissite 000-default
sudo a2dissite default-ssl
Next, we can enable the default site and reload Apache2, also using Bash. Remember to replace femmebabe with the name of the file you declared when editing in /etc/apache2/sites-available/.

sudo a2ensite femmebabe-le-ssl
sudo systemctl reload apache2
Go back to your domain in the navbar. You should see the site you configured in your web browser. Congratulations! If you don't see it, you may need to make some changes. Carefully review the settings in your project, apache configuration, and make sure you don't have any errors, and run the following commands to check the project for errors.

cd projectname
source venv/bin/activate
python manage.py check
If you have errors in your python project, trace them to where they are and fix them. You might not be able to see all of your errors depending on where they are, so if you have an error that simply says “populate isn't reentrant”, edit the following file in the virtual environment, registry.py, to expose the error.

nano venv/lib/python3.12/site-packages/django/apps/registry.py
Scroll to line 83, where this runtime error is raised (raise RuntimeError(“populate() isn't reentrant”)), and add a comment before this line, then adding, with the same indentation, self.app_configs = {}. This looks like this:

            if self.loading:
                #  Prevent reentrant calls to avoid running AppConfig.ready()
                #  methods twice.
#                 raise RuntimeError("populate() isn't reentrant")
                self.app_configs = {}
            self.loading = True
You can then check the project again and expose the error.

python manage.py check
Then you can see the error and fix it. When you have it fixed and the code compiles with no errors, make sure to change the file back so it looks like this:

            if self.loading:
                #  Prevent reentrant calls to avoid running AppConfig.ready()
                #  methods twice.
                raise RuntimeError("populate() isn't reentrant")
#                 self.app_configs = {}
            self.loading = True
Provided the server is online, when we make any further changes to it, we need to use the following command to reload the server:

sudo systemctl reload apache2
Awesome! But what about sending mail? To begin sending email, we will first need to update the domain configuration. This should be in your DNS panel in SquareSpace, or whatever domain name registrar you chose. We will also need to install and add configuration, and run a few commands. First, let's get the IPV6 address of the server. We'll then open up your DNS and add the records. To get the server's IPV6 address, use this command:

ip -6 addr
Now, we can add the following records to the DNS settings. My records look like this. However, for your records, you should replace the IP address with your IP (not 75.147.182.214, that's mine). Also add your domain in place of femmebabe.com, as well as your IPV6 address found with the previous command (you can't use mine, fe80::725a:fff:fe49:3e02). Don't worry about the domainkey for now, this is created when we set up postfix, the mail server, with OpenDKIM, and print the key. We will configure this last. @ A N/A 75.147.182.214 @ MX 10 femmebabe.com @ PTR N/A femmebabe.com @ TXT N/A TXT @ v=spf1 mx ip75.147.182.214ip6:fe80::725a:fff:fe49:3e02 ~all default._bimi TXT N/A v=BIMI1;l=https://femmebabe.com/media/static/femmebabe.svg _dmarc TXT N/A v=DMARC1; p=none sendonly._domainkey TXT N/A Now, we'll need to add some persisting configuration for postfix. All we need to do is make sure we replace the domain name, femmebabe.com, with the domain name you are using. Let's look at all the config files one by one, and install them in a directory called config in our project, for install to the OS.

nano config/etc_postfix_main.cf
Add this text to the file

#  See /usr/share/postfix/main.cf.dist for a commented, more complete version


#  Debian specific:  Specifying a file name will cause the first
#  line of that file to be used as the name.  The Debian default
# IS /etc /mailname.
# myorigin = /etc/mailname

smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
biff = no

#  appending .domain is the MUA's job.
append_dot_mydomain = no

#  Uncomment the next line to generate "delayed mail" warnings
# delay_warning_time = 4h

readme_directory = no

#  See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on
#  fresh installs.
compatibility_level = 3.6



# TLS parameters
smtpd_tls_cert_file=/etc/letsencrypt/live/femmebabe.com/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/femmebabe.com/privkey.pem
smtpd_tls_security_level=may

smtp_tls_CApath=/etc/ssl/certs
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache

smtpd_relay_restrictions = permit_sasl_authenticated, defer_unauth_destination
myhostname = femmebabe.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = femmebabe.com, localhost, $myhostname
smtp_helo_name = femmebabe.com
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all

# Milter configuration
milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:/opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters

smtp_tls_security_level = encrypt
smtp_tls_loglevel = 1

virtual_transport=lmtp:unix:private/dovecot-lmtp

smtpd_sasl_path = private/auth
Next config!

nano config/etc_postfix_master.cf
Add these lines:

# 
#  Postfix master process configuration file.  For details on the format
#  of the file, see the master(5) manual page (command: "man 5 master" or
# On-line: http://www.postfix.org/master.5.html).
# 
#  Do not forget to execute "postfix reload" after editing this file.
# 
#  ==========================================================================
#  service type  private unpriv  chroot  wakeup  maxproc command + args
#                (yes)   (yes)   (no)    (never) (100)
#  ==========================================================================
smtp      inet  n       -       y       -       -       smtpd
# smtp      inet  n       -       y       -       1       postscreen
# SMTPD Pass - - Y - - SMTPD
# DNSBLOG UNIX - - Y - 0 DNSBLOG
# tlsproxy  unix  -       -       y       -       0       tlsproxy
#  Choose one: enable submission for loopback clients only, or for any client.
# 127.0.0.1:submission inet n -   y       -       -       smtpd
submission inet n       -       y       -       -       smtpd
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_tls_wrappermode=no
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
#   -o syslog_name=postfix/submission
#   -o smtpd_tls_security_level=encrypt
#   -o smtpd_sasl_auth_enable=yes
#   -o smtpd_tls_auth_only=yes
#   -o smtpd_reject_unlisted_recipient=no
# -O smtpd_client_restrictions = $ mua_client_restrictions
#   -o smtpd_helo_restrictions=$mua_helo_restrictions
#   -o smtpd_sender_restrictions=$mua_sender_restrictions
#   -o smtpd_recipient_restrictions=
#   -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
# -O MILTER_MACRO_DAEmon_Name = Originating
#  Choose one: enable smtps for loopback clients only, or for any client.
# 127.0.0.1:SMTPS INET N - Y - - SMTPD
# SMTPS INET N - Y - - SMTPD
# -O Syslog_Name = PostFix/SMTPS
#   -o smtpd_tls_wrappermode=yes
#   -o smtpd_sasl_auth_enable=yes
#   -o smtpd_reject_unlisted_recipient=no
# -O smtpd_client_restrictions = $ mua_client_restrictions
#   -o smtpd_helo_restrictions=$mua_helo_restrictions
#   -o smtpd_sender_restrictions=$mua_sender_restrictions
# -o smtpd_recipient_trastrictions =
#   -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
# -O MILTER_MACRO_DAEmon_Name = Originating
# 628 Inet N - Y - - QMQPD
pickup    unix  n       -       y       60      1       pickup
cleanup   unix  n       -       y       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
# qmgr unix n - n 300 1 oqmg
tlsmgr    unix  -       -       y       1000?   1       tlsmgr
rewrite   unix  -       -       y       -       -       trivial-rewrite
bounce    unix  -       -       y       -       0       bounce
defer     unix  -       -       y       -       0       bounce
trace     unix  -       -       y       -       0       bounce
verify    unix  -       -       y       -       1       verify
flush     unix  n       -       y       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       y       -       -       smtp
relay     unix  -       -       y       -       -       smtp
        -o syslog_name=postfix/$service_name
#        -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
showq     unix  n       -       y       -       -       showq
error     unix  -       -       y       -       -       error
retry     unix  -       -       y       -       -       error
discard   unix  -       -       y       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       y       -       -       lmtp
anvil     unix  -       -       y       -       1       anvil
scache    unix  -       -       y       -       1       scache
postlog   unix-dgram n  -       n       -       1       postlogd
# 
#  ====================================================================
#  Interfaces to non-Postfix software. Be sure to examine the manual
#  pages of the non-Postfix software to find out what options it wants.
# 
#  Many of the following services use the Postfix pipe(8) delivery
#  agent.  See the pipe(8) woman page for information about ${recipient}
#  and other message envelope options.
#  ====================================================================
# 
#  maildrop. See the Postfix MAILDROP_README file for details.
#  Also specify in main.cf: maildrop_destination_recipient_limit=1
# 
maildrop  unix  -       n       n       -       -       pipe
  flags=DRXhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
# 
#  ====================================================================
# 
#  Recent Cyrus versions can use the existing "lmtp" master.cf entry.
# 
#  Specify in cyrus.conf:
#    lmtp    cmd="lmtpd -a" listen="localhost:lmtp" proto=tcp4
# 
#  Specify in main.cf one or more of the following:
#   mailbox_transport = lmtp:inet:localhost
# Virtual_Transport = LMTP: INET: LocalHost
# 
#  ====================================================================
# 
# Cyrus 2.1.5 (Amos Gouaux)
#  Also specify in main.cf: cyrus_destination_recipient_limit=1
# 
# Cyrus Unix - N N - - Pipe
# flags = drx user = Cyrus arg =/Cyrus/bin/Deliver -e -r $ {SENDER} -M $ {Extension} $ {User}
# 
#  ====================================================================
#  Old example of delivery via Cyrus.
# 
# Old -Cyrus Unix - N N - - Pipe
#   flags=R user=cyrus argv=/cyrus/bin/deliver -e -m ${extension} ${user}
# 
#  ====================================================================
# 
#  See the Postfix UUCP_README file for configuration details.
# 
uucp      unix  -       n       n       -       -       pipe
  flags=Fqhu user=uucp argv=uux -r -n -z -a$sender - $nexthop!rmail ($recipient)
# 
#  Other external delivery methods.
# 
ifmail    unix  -       n       n       -       -       pipe
  flags=F user=ftn argv=/usr/lib/ifmail/ifmail -r $nexthop ($recipient)
bsmtp     unix  -       n       n       -       -       pipe
  flags=Fq. user=bsmtp argv=/usr/lib/bsmtp/bsmtp -t$nexthop -f$sender $recipient
scalemail-backend unix -       n       n       -       2       pipe
  flags=R user=scalemail argv=/usr/lib/scalemail/bin/scalemail-store ${nexthop} ${user} ${extension}
mailman   unix  -       n       n       -       -       pipe
  flags=FRX user=list argv=/usr/lib/mailman/bin/postfix-to-mailman.py ${nexthop} ${user}
And the opendkim configuration. OpenDKIM identifies email servers with domain keys to make them more secure. Without it, mail isn't signed and might not make it to an inbox.

nano config/etc_default_opendkim
Add these lines:

#  NOTE: This is a legacy configuration file. It is not used by the opendkim
#  systemd service. Please use the corresponding configuration parameters in
#  /etc/opendkim.conf instead.
# 
#  Previously, one would edit the default settings here, and then execute
# /lib/opendkim/opendkim.service.Generate to generate Systemd Override Files to
#  /etc/systemd/system/opendkim.service.d/override.conf and
#  /etc/tmpfiles.d/opendkim.conf. While this is still possible, it is now
#  recommended to adjust the settings directly in /etc/opendkim.conf.
# 
# Daemon_opts = ""
#  Change to /var/spool/postfix/run/opendkim to use a Unix socket with
#  postfix in a chroot:
# Rundir =/was/spool/postfix/Run/Opendkim
RUNDIR=/run/opendkim
# 
#  Uncomment to specify an alternate socket
#  Note that setting this will override any Socket value in opendkim.conf
# default:
SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"
#  listen on all interfaces on port 54321:
# Socket = inet: 54321
#  listen on loopback on port 12345:
# Socket = inet: 12345@localhost
# Listen is 192.0.2.1 is Port 12345:
# Socket = inet: 12345@192.0.2.1
USER=opendkim
GROUP=opendkim
PIDFILE=$RUNDIR/$NAME.pid
EXTRAAFTER=

nano config/etc_dovecot_conf.d_10-master.conf
Add these lines:

0-master.conf 
# default_process_limit = 100
# default_client_limit = 1000

#  Default VSZ (virtual memory size) limit for service processes. This is mainly
#  intended to catch and kill processes that leak memory before they eat up
#  everything.
# default_vsz_limit = 256m

#  Login user is internally used by login processes. This is the most untrusted
#  user in Dovecot system. It shouldn't have access to anything at all.
# default_login_user = dovenull

#  Internal user is used by unprivileged processes. It should be separate from
#  login user, so that login processes can't disturb other processes.
# default_internal_user = dovecot

service imap-login {
  inet_listener imap {
    # Port = 143
  }
  inet_listener imaps {
    # Port = 993
    # ssl = Yes
  }

  #  Number of connections to handle before starting a new process. Typically
  #  the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
  #  is faster. <doc/wiki/LoginProcess.txt>
  # service_count = 1

  #  Number of processes to always keep waiting for more connections.
  # Process_min_avail = 0

  #  If you set service_count=0, you probably need to grow this.
  # VSZ_LIMI = $ Default_vsz_Limit
}

service pop3-login {
  inet_listener pop3 {
    # Port = 110
  }
  inet_listener pop3s {
    # Port = 995
    # ssl = Yes
  }
}

service submission-login {
  inet_listener submission {
    # Port = 587
  }
}

service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    group = postfix
    mode = 0666
    user = postfix
  }

  #  Create inet listener only if you can't use the above UNIX socket
  # inet_lister lmtp {
    #  Avoid making LMTP visible for the entire internet
    # Address =
    # port =
  # }
}

service imap {
  #  Most of the memory goes to mmap()ing files. You may need to increase this
  #  limit if you have huge mailboxes.
  # vsz_limit = $ default_vsz_limit

  #  Max. number of IMAP processes (connections)
  # Process_limit = 1024
}

service pop3 {
  #  Max. number of POP3 processes (connections)
  # process_limit = 1024
}

service submission {
  #  Max. number of SMTP Submission processes (connections)
  # process_limit = 1024
}

service auth {
  #  auth_socket_path points to this userdb socket by default. It's typically
  #  used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
  #  full permissions to this socket are able to get a list of all usernames and
  #  get the results of everyone's userdb lookups.
  # 
  #  The default 0666 mode allows anyone to connect to the socket, but the
  #  userdb lookups will succeed only if the userdb returns an "uid" field that
  #  matches the caller process's UID. Also if caller's uid or gid matches the
  #  socket's uid or gid the lookup succeeds. Anything else causes a failure.
  # 
  #  To give the caller full permissions to lookup all users, set the mode to
  #  something else than 0666 and Dovecot lets the kernel enforce the
  #  permissions (e.g. 0777 allows everyone full permissions).
  unix_listener /var/spool/postfix/private/auth {
    mode = 0660
    user = postfix
    group = postfix
  }
}

service auth-worker {
  #  Auth worker process is run as root by default, so that it can access
  #  /etc/shadow. If this isn't necessary, the user should be changed to
  # $ default_interal_user.
  # User = root
}

service dict {
  #  If dict proxy is used, mail processes should have access to its socket.
  #  For example: mode=0660, group=vmail and global mail_access_groups=vmail
  unix_listener dict {
    # Mode = 0600
    # User =
    # group = 
  }
}
Once again, make sure to replace the domain in all of these files, femmebabe.com, with the domain you selected. Edit the next file, dovecot's config,

nano config/etc_dovecot_dovecot
And add these lines

## Ovecot Configuration file

#  If you're in a hurry, see http://wiki2.dovecot.org/QuickConfiguration

#  "doveconf -n" command gives a clean output of the changed settings. Use it
#  instead of copy&pasting files when posting to the Dovecot mailing list.

# '# ' character and everything after it is treated as comments. Extra spaces
#  and tabs are ignored. If you want to use either of these explicitly, put the
# value inside quotes, eg.: key = "#  char and trailing whitespace  "

#  Most (but not all) settings can be overridden by different protocols and/or
#  source/destination IPs by placing the settings inside sections, for example:
# Protocol Imap {}, Local 127.0.0.1 {}, Remote 10.0.0.0/8 {}

#  Default values are shown for each setting, it's not required to uncomment
#  those. These are exceptions to this though: No sections (e.g. namespace {})
#  or plugin settings are added by default, they're listed only as examples.
#  Paths are also just examples with the real defaults being based on configure
#  options. The paths listed here are for configure --prefix=/usr
#  --sysconfdir=/etc --localstatedir=/var

#  Enable installed protocols
!include_try /usr/share/dovecot/protocols.d/*.protocol

#  A comma separated list of IPs or hosts where to listen in for connections. 
#  "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.
#  If you want to specify non-default ports or anything more complex,
#  edit conf.d/master.conf.
# Listen = *, ::

#  Base directory where to store runtime data.
# base_dir =/var/run/dovecot/

#  Name of this instance. In multi-instance setup doveadm and other commands
#  can use -i <instance_name> to select which instance is used (an alternative
#  to -c <config_path>). The instance name is also added to Dovecot processes
#  in ps output.
# Instance_name = Dovecot

#  Greeting message for clients.
# login_greeting = Dovecot ready.

#  Space separated list of trusted network ranges. Connections from these
#  IPs are allowed to override their IP addresses and ports (for logging and
#  for authentication checks). disable_plaintext_auth is also ignored for
#  these networks. Typically you'd specify your IMAP proxy servers here.
# login_trusted_networks =

#  Space separated list of login access check sockets (e.g. tcpwrap)
# login_access_sockets = 

#  With proxy_maybe=yes if proxy destination matches any of these IPs, don't do
#  proxying. This isn't necessary normally, but may be useful if the destination
#  IP is e.g. a load balancer's IP.
# auth_proxy_self =

#  Show more verbose process titles (in ps). Currently shows user name and
#  IP address. Useful for seeing who are actually using the IMAP processes
#  (eg. shared mailboxes or if same uid is used for multiple accounts).
# verbose_proctle = no

#  Should all processes be killed when Dovecot master process shuts down.
#  Setting this to "no" means that Dovecot can be upgraded without
#  forcing existing client connections to close (although that could also be
#  a problem if the upgrade is e.g. because of a security fix).
# shutdown_clients = yes

#  If non-zero, run mail commands via this many connections to doveadm server,
#  instead of running them directly in the same process.
# doveadm_worker_count = 0
#  UNIX socket or host:port used for connecting to doveadm server
# doveadm_socket_path = doveadm-server

#  Space separated list of environment variables that are preserved on Dovecot
#  startup and passed down to all of its child processes. You can also give
#  key=value pairs to always set specific settings.
# Import_environment = TZ

## 
##  Dictionary server settings
## 

#  Dictionary can be used to store key=value lists. This is used by several
#  plugins. The dictionary can be accessed either directly or though a
#  dictionary server. The following dict block maps dictionary names to URIs
#  when the server is used. These can then be referenced using URIs in format
# "Proxy :: <Name>".

dict {
  # quota = MySQL:/andc/dovecot/dovecot-dic-sql.conf.ext
}

#  Most of the actual configuration gets included below. The filenames are
#  first sorted by their ASCII value and parsed in that order. The 00-prefixes
#  in filenames are intended to make it easier to understand the ordering.
!include conf.d/*.conf

#  A config file can also tried to be included without giving an error if
#  it's not found:
!include_try local.conf

passdb {
  driver = passwd-file
  args = /etc/dovecot/passwd
}
userdb {
  driver = passwd
}

protocols = imap pop3

# Allows Dovecot to listen to all input connections (ipv4 / ipv6)

listen = *, ::
Add a password for the dovecot user:

nano config/etc_dovecot_passwd
The first part of the file, before the colon, is the username. The last part, “yourpassword”, denotes the password you would like to give your mail server.

team:{plain}yourpassword
Next, the OpenDKIM config

nano config/etc_opendkim.conf
And add these lines:

#  This is a basic configuration for signing and verifying. It can easily be
#  adapted to suit a basic installation. See opendkim.conf(5) and
#  /usr/share/doc/opendkim/examples/opendkim.conf.sample for complete
# Documentation of Available Configuration Parameters.

Syslog			yes
SyslogSuccess		yes
# Logwhy no

#  Common signing and verification parameters. In Debian, the "From" header is
#  oversigned, because it is often the identity key used by reputation systems
#  and thus somewhat security sensitive.
Canonicalization	relaxed/simple
Mode			s
SubDomains		no
OversignHeaders		From

#  Signing domain, selector, and key (required). For example, perform signing
#  for domain "example.com" with selector "2020" (2020._domainkey.example.com),
#  using the private key stored in /etc/dkimkeys/example.private. More granular
#  setup options can be found in /usr/share/doc/opendkim/README.opendkim.
# Domain Example.com
# Selector		2020
# Keyfile /etc/dkimkeys/example.private

#  In Debian, opendkim runs as user "opendkim". A umask of 007 is required when
#  using a local socket with MTAs that access the socket as a non-privileged
#  user (for example, Postfix). You may need to add user "postfix" to group
#  "opendkim" in that case.
UserID			opendkim
UMask			007

#  Socket for the MTA connection (required). If the MTA is inside a chroot jail,
#  it must be ensured that the socket is accessible. In Debian, Postfix runs in
#  a chroot in /var/spool/postfix, therefore a Unix socket would have to be
#  configured as shown on the last line below.
# Socket Local: /run/opendkim/opendkim.sock
# Socket inet: 8891@Localhost
# Socket inet: 8891
Socket			local:/var/spool/postfix/opendkim/opendkim.sock

PidFile			/run/opendkim/opendkim.pid

#  Hosts for which to sign rather than verify, default is 127.0.0.1. See the
#  OPERATION section of opendkim(8) for more information.
# InternalHosts		192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12

#  The trust anchor enables DNSSEC. In Debian, the trust anchor file is provided
#  by the package dns-root-data.
TrustAnchorFile		/usr/share/dns/root.key
# Nameservers 127.0.0.1

#  Map domains in From addresses to keys used to sign messages
KeyTable           refile:/etc/opendkim/key.table
SigningTable       refile:/etc/opendkim/signing.table

#  A set of internal hosts whose mail should be signed
InternalHosts       /etc/opendkim/trusted.hosts

nano config/etc_default_opendkim
And add these lines

#  NOTE: This is a legacy configuration file. It is not used by the opendkim
#  systemd service. Please use the corresponding configuration parameters in
#  /etc/opendkim.conf instead.
# 
#  Previously, one would edit the default settings here, and then execute
# /lib/opendkim/opendkim.service.Generate to generate Systemd Override Files to
#  /etc/systemd/system/opendkim.service.d/override.conf and
#  /etc/tmpfiles.d/opendkim.conf. While this is still possible, it is now
#  recommended to adjust the settings directly in /etc/opendkim.conf.
# 
# Daemon_opts = ""
#  Change to /var/spool/postfix/run/opendkim to use a Unix socket with
#  postfix in a chroot:
# Rundir =/Var/Spool/Postfix/Run/Opendkim
RUNDIR=/run/opendkim
# 
#  Uncomment to specify an alternate socket
#  Note that setting this will override any Socket value in opendkim.conf
# default:
SOCKET="local:/var/spool/postfix/opendkim/opendkim.sock"
#  listen on all interfaces on port 54321:
# Socket = inet: 54321
#  listen on loopback on port 12345:
# Socket = inet: 12345@localhost
# Listen is 192.0.2.1 is Port 12345:
# Socket = inet: 12345@192.0.2.1
USER=opendkim
GROUP=opendkim
PIDFILE=$RUNDIR/$NAME.pid
EXTRAAFTER=
When we are ready to set up our Postfix server, we will run the below code, with the appropriate domain name embedded. Start by creating a script

touch scripts/postfixsetup
sudo chmod a+x scripts/postfixsetup
nano scripts/postfixsetup
Now, in nano, the text editor, edit this file so it includes your domain name instead of femmebabe.com.

# ! / bin / Bash
# Setup postfix
cd $DIR
echo "Mail services configuration"
sudo cp /etc/postfix/main.cf /etc/postfix/main.cf.backup
sudo cp config/etc_postfix_main.cf /etc/postfix/main.cf
sudo cp config/etc_postfix_master.cf /etc/postfix/master.cf
sudo cp config/etc_default_opendkim /etc/default/opendkim
sudo cp config/etc_dovecot_conf.d_10-auth.conf /etc/dovecot/conf.d/10-auth.conf
sudo cp config/etc_dovecot_conf.d_10-master.conf /etc/dovecot/conf.d/10-master.conf
sudo cp config/etc_dovecot_dovecot.conf /etc/dovecot/dovecot.conf
sudo cp config/etc_dovecot_passwd /etc/dovecot/passwd
sudo cp config/etc_opendkim.conf /etc/opendkim.conf
sudo cp config/etc_default_opendkim /etc/default/opendkim
sudo adduser postfix opendkim
sudo mkdir /etc/opendkim
sudo mkdir /etc/opendkim/keys
sudo mkdir /etc/opendkim/keys/femmebabe.com
sudo mkdir /var/spool/postfix/opendkim
sudo echo "*@femmebabe.com     sendonly._domainkey.femmebabe.com" | sudo tee -a /etc/opendkim/signing.table
sudo echo "sendonly._domainkey.femmebabe.com    femmebabe.com:sendonly:/etc/opendkim/keys/femmebabe.com/sendonly.private" | sudo tee -a /etc/opendkim/key.table
sudo echo "127.0.0.1" | sudo tee -a /etc/opendkim/trusted.hosts
sudo echo "localhost" | sudo tee -a /etc/opendkim/trusted.hosts
sudo echo "" | sudo tee -a /etc/opendkim/trusted.hosts
sudo echo "*.femmebabe.com" | sudo tee -a /etc/opendkim/trusted.hosts
sudo chown -R opendkim:opendkim /etc/opendkim
sudo opendkim-genkey -b 2048 -d femmebabe.com -D /etc/opendkim/keys/femmebabe.com -s sendonly -v
sudo chmod go-rw /etc/opendkim/keys
sudo chown opendkim:opendkim /etc/opendkim/keys/femmebabe.com/sendonly.private
sudo chown opendkim:postfix /var/spool/postfix/opendkim
cd $DIR
sudo cp mailbox/* /var/mail/
sudo chown :users /var/mail/*
sudo chmod -R a+rwx /var/mail/*
sudo systemctl restart opendkim postfix dovecot
sudo cat /etc/opendkim/keys/femmebabe.com/sendonly.txt | tr -d '\n' | sed 's/\s//g' | sed 's/""//g' | awk -F'[)(]' '{print $2}'
Now, run the completed script to configure postfix, opendkim and dovecot.

./scripts/postfixsetup
Once this script has run, copy the last line it prints and paste it into your DNS configuration as the value for sendonly._domainkey. This is the OpenDKIM key used to identify your domain when sending secure mail. Awesome! Within a few days, you should be able to send mail from the server provided everything is configured correctly. If you just configured the DNS for your mail server, it should take less than 72 hours for the records to update. It's usually much quicker. You can check if your server is working by using this command, supplied your email:

echo “test” | mail -s “Test Email” youremail@gmail.com
If everything appears to be working correctly, you should be able to send email with your server. If it's not working, try looking at the logs to see what the error might be.

tail –lines 150 /var/log/mail.log
This will offer verbose information about mail that's being sent by the server and whether it is working properly. You should be able to see the email in your inbox as well, if it's not there, check your spam folder. You will also need to configure your settings in your settings.py so your email server can talk to your Django app, the project. Add or replace these lines in your settings

EMAIL_HOST = DOMAIN
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_ADDRESS = 'team@femmebabe.com'
EMAIL_HOST_USER = 'team' # 'Love@MamaSheen.com'
EMAIL_HOST_PASSWORD = config['EMAIL_HOST_PASSWORD']
DEFAULT_FROM_EMAIL = '{} <{}>'.format(SITE_NAME, EMAIL_HOST_USER)
Notice that we are using a config file to get the password. Let's load this file in the settings like so, at the very beginning of the file.:

import os
import json

#  Open and load config
with open('/etc/config.json') as config_file:
    config = json.load(config_file)
Let's create this file and add a secret key to it, as well as the mail password. To generate a secret key, use this command, with whatever length you like at the end:

openssl rand -base64 64
Now, copy the text that openssl generated and edit /etc/config.json

sudo nano /etc/config.json
Add the following lines to your file, with the key that openssl generated as the secret key.

{
	"SECRET_KEY": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX-generated-using-openssl)",
	"EMAIL_HOST_PASSWORD": "yourpassword"
}
Json format is simple and easy to use, we can declare other keys we want to use in our project this way too, and keep them separate from our project directory so other users can't write to them and so they can't be read from our project directory alone. This is recommended practice for API keys, of which we will use more than a few here. You'll also want to back up your project to make sure everything is saved and you'll be able to recover your work later even if you no longer wish to rent a server.

sudo backup
Now, try sending an HTML email from the web server, provided sending one from the command line is working. Query your user instance in the shell, and send an html email to that user through Django. Change my name in the code, Charlotte, to your username.

python manage.py shell
from django.contrib.auth.models import User
u = User.objects.get(username='Charlotte')
from users.email import send_welcome_email
send_welcome_email(u)
exit()
If the first command doesn't work, make sure to use

source venv/bin/activate
Provided everything is set up correctly, you will now get a welcome email in your mailbox sent by your web app. Good job! You've come a long way. I wanted to add, if you are ever struggling with any errors at all while working on a project like this, don't hesitate to search for answers and ask for help. Google, among other search engines, are great resources to search for programming help. Simply search for the error you are getting, and you will be able to see how other people solve the problem. Also, you're welcome to contact me, your educators (teachers, professors, tutors), any peers on the internet who are available for programming help, or consult this book again or other resources to find solutions to the issues you are experiencing. I understand this isn't easy, but even if you have read in this far and aren't writing any code, you are learning a lot about building a web app from scratch. Pat yourself on the back, you're doing a great job. Thank you for taking the time to read this third edition web development guide. In future editions, I will include more of the important examples discussed in the beginning of the document and we will dive much deeper into the world of software and hardware development. Stay tuned for what's to come, and I look forward to teaching you how to build incredible software. See you in the next edition!






Close
Page 1
Jump
See full article
Continue reading

Buy | Buy with crypto



https://glamgirlx.com/en/practical-web-based-deep -


(Click or tap to download image)
Professional entertainment, photos, videos, audio, livestreaming and casual gameplay, as well as ID scanning, web development and surrogacy services.

Leave me a tip in Bitcoin using this address: 3KhDWoSve2N627RiW8grj6XrsoPT7d6qyE

© Glam Girl X 2025

Terms of Service