2018-12-24 17:45:15 +00:00
/ *
* Copyright © 2018 A Bunch Tell LLC .
*
* This file is part of WriteFreely .
*
* WriteFreely is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , included
* in the LICENSE file in this source code package .
* /
2018-12-31 06:05:26 +00:00
2018-10-15 18:44:15 +00:00
package writefreely
import (
2018-10-17 02:31:27 +00:00
"database/sql"
2018-10-16 20:57:55 +00:00
"flag"
2018-10-15 18:44:15 +00:00
"fmt"
2018-11-10 02:14:22 +00:00
"html/template"
2018-10-15 18:44:15 +00:00
"net/http"
2018-11-14 22:47:58 +00:00
"net/url"
2018-10-15 18:44:15 +00:00
"os"
"os/signal"
2019-01-18 23:57:04 +00:00
"path/filepath"
2018-11-08 05:11:42 +00:00
"regexp"
2018-11-13 18:04:52 +00:00
"strings"
2018-10-15 18:44:15 +00:00
"syscall"
2018-11-10 03:16:13 +00:00
"time"
2018-10-15 18:44:15 +00:00
2018-11-26 20:11:10 +00:00
_ "github.com/go-sql-driver/mysql"
2018-10-15 18:44:15 +00:00
"github.com/gorilla/mux"
2018-11-08 06:31:01 +00:00
"github.com/gorilla/schema"
2018-10-15 18:44:15 +00:00
"github.com/gorilla/sessions"
2018-11-14 20:03:22 +00:00
"github.com/manifoldco/promptui"
2018-11-21 20:04:47 +00:00
"github.com/writeas/go-strip-markdown"
2018-12-06 02:41:51 +00:00
"github.com/writeas/web-core/auth"
2018-11-08 06:31:01 +00:00
"github.com/writeas/web-core/converter"
2018-10-15 18:44:15 +00:00
"github.com/writeas/web-core/log"
2018-12-20 02:26:13 +00:00
"github.com/writeas/writefreely/author"
2018-10-15 18:44:15 +00:00
"github.com/writeas/writefreely/config"
2019-01-17 18:53:03 +00:00
"github.com/writeas/writefreely/migrations"
2018-11-08 04:50:50 +00:00
"github.com/writeas/writefreely/page"
2018-10-15 18:44:15 +00:00
)
const (
2019-01-18 23:57:04 +00:00
staticDir = "static"
2018-11-08 06:31:01 +00:00
assumedTitleLen = 80
postsPerPage = 10
2018-10-17 22:57:37 +00:00
2018-11-08 18:37:42 +00:00
serverSoftware = "WriteFreely"
2018-10-17 22:57:37 +00:00
softwareURL = "https://writefreely.org"
2018-10-15 18:44:15 +00:00
)
2018-11-08 03:13:16 +00:00
var (
debugging bool
2018-11-08 06:31:01 +00:00
2018-11-26 13:37:06 +00:00
// Software version can be set from git env using -ldflags
2019-04-12 01:04:48 +00:00
softwareVer = "0.9.0"
2018-11-26 13:37:06 +00:00
2018-11-08 06:31:01 +00:00
// DEPRECATED VARS
// TODO: pass app.cfg into GetCollection* calls so we can get these values
// from Collection methods and we no longer need these.
hostName string
isSingleUser bool
2018-11-08 03:13:16 +00:00
)
2018-10-15 18:44:15 +00:00
type app struct {
router * mux . Router
2018-10-17 02:31:27 +00:00
db * datastore
2018-10-15 18:44:15 +00:00
cfg * config . Config
2018-12-08 22:49:19 +00:00
cfgFile string
2018-10-15 18:44:15 +00:00
keys * keychain
sessionStore * sessions . CookieStore
2018-11-08 06:31:01 +00:00
formDecoder * schema . Decoder
2018-12-10 21:02:42 +00:00
timeline * localTimeline
2018-10-15 18:44:15 +00:00
}
2018-11-08 05:11:42 +00:00
// handleViewHome shows page at root path. Will be the Pad if logged in and the
// catch-all landing page otherwise.
func handleViewHome ( app * app , w http . ResponseWriter , r * http . Request ) error {
if app . cfg . App . SingleUser {
// Render blog index
return handleViewCollection ( app , w , r )
}
// Multi-user instance
u := getUserSession ( app , r )
if u != nil {
// User is logged in, so show the Pad
return handleViewPad ( app , w , r )
}
2018-11-10 02:14:22 +00:00
p := struct {
page . StaticPage
Flashes [ ] template . HTML
} {
StaticPage : pageForReq ( app , r ) ,
}
// Get error messages
session , err := app . sessionStore . Get ( r , cookieName )
if err != nil {
// Ignore this
log . Error ( "Unable to get session in handleViewHome; ignoring: %v" , err )
}
flashes , _ := getSessionFlashes ( app , w , r , session )
for _ , flash := range flashes {
p . Flashes = append ( p . Flashes , template . HTML ( flash ) )
}
2018-11-08 05:11:42 +00:00
// Show landing page
2018-11-10 02:14:22 +00:00
return renderPage ( w , "landing.tmpl" , p )
2018-11-08 05:11:42 +00:00
}
2018-11-19 02:58:50 +00:00
func handleTemplatedPage ( app * app , w http . ResponseWriter , r * http . Request , t * template . Template ) error {
p := struct {
page . StaticPage
2019-04-11 17:56:07 +00:00
ContentTitle string
2018-11-21 20:04:47 +00:00
Content template . HTML
PlainContent string
Updated string
2018-11-21 19:05:44 +00:00
AboutStats * InstanceStats
2018-11-19 02:58:50 +00:00
} {
StaticPage : pageForReq ( app , r ) ,
}
if r . URL . Path == "/about" || r . URL . Path == "/privacy" {
2019-04-06 17:23:22 +00:00
var c * instanceContent
2018-11-19 02:58:50 +00:00
var err error
if r . URL . Path == "/about" {
c , err = getAboutPage ( app )
2018-11-21 19:05:44 +00:00
// Fetch stats
p . AboutStats = & InstanceStats { }
p . AboutStats . NumPosts , _ = app . db . GetTotalPosts ( )
p . AboutStats . NumBlogs , _ = app . db . GetTotalCollections ( )
2018-11-19 02:58:50 +00:00
} else {
2019-04-06 17:23:22 +00:00
c , err = getPrivacyPage ( app )
2018-11-19 02:58:50 +00:00
}
if err != nil {
return err
}
2019-04-11 17:56:07 +00:00
p . ContentTitle = c . Title . String
2019-04-06 17:23:22 +00:00
p . Content = template . HTML ( applyMarkdown ( [ ] byte ( c . Content ) , "" ) )
p . PlainContent = shortPostDescription ( stripmd . Strip ( c . Content ) )
if ! c . Updated . IsZero ( ) {
p . Updated = c . Updated . Format ( "January 2, 2006" )
2018-11-19 02:58:50 +00:00
}
}
// Serve templated page
err := t . ExecuteTemplate ( w , "base" , p )
if err != nil {
log . Error ( "Unable to render page: %v" , err )
}
return nil
}
2018-11-08 04:50:50 +00:00
func pageForReq ( app * app , r * http . Request ) page . StaticPage {
p := page . StaticPage {
AppCfg : app . cfg . App ,
Path : r . URL . Path ,
2018-11-20 17:14:02 +00:00
Version : "v" + softwareVer ,
2018-11-08 04:50:50 +00:00
}
// Add user information, if given
var u * User
accessToken := r . FormValue ( "t" )
if accessToken != "" {
userID := app . db . GetUserID ( accessToken )
if userID != - 1 {
var err error
u , err = app . db . GetUserByID ( userID )
if err == nil {
p . Username = u . Username
}
}
} else {
u = getUserSession ( app , r )
if u != nil {
p . Username = u . Username
}
}
return p
}
2018-10-15 18:44:15 +00:00
var shttp = http . NewServeMux ( )
2018-11-08 05:11:42 +00:00
var fileRegex = regexp . MustCompile ( "/([^/]*\\.[^/]*)$" )
2018-10-15 18:44:15 +00:00
func Serve ( ) {
2019-01-18 16:34:25 +00:00
// General options usable with other commands
2018-11-08 03:13:16 +00:00
debugPtr := flag . Bool ( "debug" , false , "Enables debug logging." )
2019-01-18 16:34:25 +00:00
configFile := flag . String ( "c" , "config.ini" , "The configuration file to use" )
// Setup actions
2018-10-16 20:57:55 +00:00
createConfig := flag . Bool ( "create-config" , false , "Creates a basic configuration and exits" )
2018-10-25 13:15:10 +00:00
doConfig := flag . Bool ( "config" , false , "Run the configuration process" )
2018-11-11 22:52:24 +00:00
genKeys := flag . Bool ( "gen-keys" , false , "Generate encryption and authentication keys" )
2018-11-13 18:04:52 +00:00
createSchema := flag . Bool ( "init-db" , false , "Initialize app database" )
2019-01-17 18:53:03 +00:00
migrate := flag . Bool ( "migrate" , false , "Migrate the database" )
2019-01-18 16:34:25 +00:00
// Admin actions
2018-12-06 02:41:51 +00:00
createAdmin := flag . String ( "create-admin" , "" , "Create an admin with the given username:password" )
2018-12-22 15:54:08 +00:00
createUser := flag . String ( "create-user" , "" , "Create a regular user with the given username:password" )
2018-11-14 20:03:22 +00:00
resetPassUser := flag . String ( "reset-pass" , "" , "Reset the given user's password" )
2018-11-15 19:51:03 +00:00
outputVersion := flag . Bool ( "v" , false , "Output the current version" )
2018-10-16 20:57:55 +00:00
flag . Parse ( )
2018-11-08 03:13:16 +00:00
debugging = * debugPtr
2018-12-08 22:49:19 +00:00
app := & app {
cfgFile : * configFile ,
}
2018-11-10 03:16:13 +00:00
2018-11-15 19:51:03 +00:00
if * outputVersion {
fmt . Println ( serverSoftware + " " + softwareVer )
os . Exit ( 0 )
} else if * createConfig {
2018-10-16 20:57:55 +00:00
log . Info ( "Creating configuration..." )
c := config . New ( )
2018-12-08 22:49:19 +00:00
log . Info ( "Saving configuration %s..." , app . cfgFile )
err := config . Save ( c , app . cfgFile )
2018-10-24 18:21:42 +00:00
if err != nil {
log . Error ( "Unable to save configuration: %v" , err )
os . Exit ( 1 )
}
2018-10-16 20:57:55 +00:00
os . Exit ( 0 )
2018-10-25 13:15:10 +00:00
} else if * doConfig {
2018-12-08 22:49:19 +00:00
d , err := config . Configure ( app . cfgFile )
2018-10-25 13:15:10 +00:00
if err != nil {
log . Error ( "Unable to configure: %v" , err )
os . Exit ( 1 )
}
2018-11-11 01:43:00 +00:00
if d . User != nil {
2018-11-10 03:16:13 +00:00
app . cfg = d . Config
connectToDatabase ( app )
defer shutdown ( app )
2019-01-13 14:08:47 +00:00
if ! app . db . DatabaseInitialized ( ) {
2019-01-26 15:52:11 +00:00
err = adminInitDatabase ( app )
if err != nil {
log . Error ( err . Error ( ) )
os . Exit ( 1 )
}
2019-01-13 14:08:47 +00:00
}
2018-11-10 03:16:13 +00:00
u := & User {
Username : d . User . Username ,
HashedPass : d . User . HashedPass ,
Created : time . Now ( ) . Truncate ( time . Second ) . UTC ( ) ,
}
// Create blog
log . Info ( "Creating user %s...\n" , u . Username )
err = app . db . CreateUser ( u , app . cfg . App . SiteName )
if err != nil {
log . Error ( "Unable to create user: %s" , err )
os . Exit ( 1 )
}
log . Info ( "Done!" )
}
2018-10-25 13:15:10 +00:00
os . Exit ( 0 )
2018-11-11 22:52:24 +00:00
} else if * genKeys {
errStatus := 0
2019-01-20 19:18:09 +00:00
// Read keys path from config
loadConfig ( app )
// Create keys dir if it doesn't exist yet
fullKeysDir := filepath . Join ( app . cfg . Server . KeysParentDir , keysDir )
if _ , err := os . Stat ( fullKeysDir ) ; os . IsNotExist ( err ) {
err = os . Mkdir ( fullKeysDir , 0700 )
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
}
// Generate keys
initKeyPaths ( app )
2018-11-11 22:52:24 +00:00
err := generateKey ( emailKeyPath )
if err != nil {
errStatus = 1
}
err = generateKey ( cookieAuthKeyPath )
if err != nil {
errStatus = 1
}
err = generateKey ( cookieKeyPath )
if err != nil {
errStatus = 1
}
os . Exit ( errStatus )
2018-11-13 18:04:52 +00:00
} else if * createSchema {
2018-12-08 15:39:25 +00:00
loadConfig ( app )
2018-11-13 18:04:52 +00:00
connectToDatabase ( app )
defer shutdown ( app )
2019-01-26 15:52:11 +00:00
err := adminInitDatabase ( app )
if err != nil {
log . Error ( err . Error ( ) )
os . Exit ( 1 )
}
os . Exit ( 0 )
2018-12-06 02:41:51 +00:00
} else if * createAdmin != "" {
2019-01-26 16:06:58 +00:00
err := adminCreateUser ( app , * createAdmin , true )
if err != nil {
log . Error ( err . Error ( ) )
os . Exit ( 1 )
}
os . Exit ( 0 )
2018-12-22 15:54:08 +00:00
} else if * createUser != "" {
2019-01-26 16:06:58 +00:00
err := adminCreateUser ( app , * createUser , false )
if err != nil {
log . Error ( err . Error ( ) )
os . Exit ( 1 )
}
os . Exit ( 0 )
2018-11-14 20:03:22 +00:00
} else if * resetPassUser != "" {
// Connect to the database
2018-12-08 15:39:25 +00:00
loadConfig ( app )
2018-11-14 20:03:22 +00:00
connectToDatabase ( app )
defer shutdown ( app )
// Fetch user
u , err := app . db . GetUserForAuth ( * resetPassUser )
if err != nil {
log . Error ( "Get user: %s" , err )
os . Exit ( 1 )
}
// Prompt for new password
prompt := promptui . Prompt {
Templates : & promptui . PromptTemplates {
Success : "{{ . | bold | faint }}: " ,
} ,
Label : "New password" ,
Mask : '*' ,
}
newPass , err := prompt . Run ( )
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
// Do the update
log . Info ( "Updating..." )
err = adminResetPassword ( app , u , newPass )
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
log . Info ( "Success." )
2019-01-17 18:53:03 +00:00
os . Exit ( 0 )
} else if * migrate {
loadConfig ( app )
connectToDatabase ( app )
defer shutdown ( app )
err := migrations . Migrate ( migrations . NewDatastore ( app . db . DB , app . db . driverName ) )
if err != nil {
log . Error ( "migrate: %s" , err )
os . Exit ( 1 )
}
2018-11-14 20:03:22 +00:00
os . Exit ( 0 )
2018-10-16 20:57:55 +00:00
}
2018-10-15 18:44:15 +00:00
log . Info ( "Initializing..." )
2018-12-08 15:39:25 +00:00
loadConfig ( app )
2018-10-15 18:44:15 +00:00
2018-12-08 15:39:25 +00:00
hostName = app . cfg . App . Host
isSingleUser = app . cfg . App . SingleUser
2018-11-08 03:13:16 +00:00
app . cfg . Server . Dev = * debugPtr
2019-01-19 00:17:10 +00:00
err := initTemplates ( app . cfg )
if err != nil {
log . Error ( "load templates: %s" , err )
os . Exit ( 1 )
}
2018-11-08 04:50:50 +00:00
2018-10-15 18:44:15 +00:00
// Load keys
log . Info ( "Loading encryption keys..." )
2019-01-20 19:18:09 +00:00
initKeyPaths ( app )
2019-01-19 00:17:10 +00:00
err = initKeys ( app )
2018-10-15 18:44:15 +00:00
if err != nil {
log . Error ( "\n%s\n" , err )
}
// Initialize modules
2018-10-17 00:30:38 +00:00
app . sessionStore = initSession ( app )
2018-11-08 06:31:01 +00:00
app . formDecoder = schema . NewDecoder ( )
app . formDecoder . RegisterConverter ( converter . NullJSONString { } , converter . ConvertJSONNullString )
app . formDecoder . RegisterConverter ( converter . NullJSONBool { } , converter . ConvertJSONNullBool )
app . formDecoder . RegisterConverter ( sql . NullString { } , converter . ConvertSQLNullString )
app . formDecoder . RegisterConverter ( sql . NullBool { } , converter . ConvertSQLNullBool )
app . formDecoder . RegisterConverter ( sql . NullInt64 { } , converter . ConvertSQLNullInt64 )
app . formDecoder . RegisterConverter ( sql . NullFloat64 { } , converter . ConvertSQLNullFloat64 )
2018-10-17 00:30:38 +00:00
2018-10-17 02:31:27 +00:00
// Check database configuration
2019-01-05 22:51:17 +00:00
if app . cfg . Database . Type == driverMySQL && ( app . cfg . Database . User == "" || app . cfg . Database . Password == "" ) {
2018-10-17 02:31:27 +00:00
log . Error ( "Database user or password not set." )
os . Exit ( 1 )
}
if app . cfg . Database . Host == "" {
app . cfg . Database . Host = "localhost"
}
if app . cfg . Database . Database == "" {
2018-11-21 18:36:00 +00:00
app . cfg . Database . Database = "writefreely"
2018-10-17 02:31:27 +00:00
}
2018-11-10 03:16:13 +00:00
connectToDatabase ( app )
2018-10-17 02:31:27 +00:00
defer shutdown ( app )
2018-12-14 00:15:09 +00:00
// Test database connection
2018-12-15 19:14:09 +00:00
err = app . db . Ping ( )
2018-12-14 00:15:09 +00:00
if err != nil {
log . Error ( "Database ping failed: %s" , err )
}
2018-10-15 18:44:15 +00:00
r := mux . NewRouter ( )
2018-11-08 04:50:50 +00:00
handler := NewHandler ( app )
handler . SetErrorPages ( & ErrorPages {
NotFound : pages [ "404-general.tmpl" ] ,
Gone : pages [ "410.tmpl" ] ,
InternalServerError : pages [ "500.tmpl" ] ,
Blank : pages [ "blank.tmpl" ] ,
} )
2018-10-15 18:44:15 +00:00
// Handle app routes
2018-10-17 02:31:27 +00:00
initRoutes ( handler , r , app . cfg , app . db )
2018-10-15 18:44:15 +00:00
2018-12-10 21:02:42 +00:00
// Handle local timeline, if enabled
if app . cfg . App . LocalTimeline {
log . Info ( "Initializing local timeline..." )
initLocalTimeline ( app )
}
2018-10-15 18:44:15 +00:00
// Handle static files
2019-01-18 23:57:04 +00:00
fs := http . FileServer ( http . Dir ( filepath . Join ( app . cfg . Server . StaticParentDir , staticDir ) ) )
2018-10-15 18:44:15 +00:00
shttp . Handle ( "/" , fs )
r . PathPrefix ( "/" ) . Handler ( fs )
// Handle shutdown
c := make ( chan os . Signal , 2 )
signal . Notify ( c , os . Interrupt , syscall . SIGTERM )
go func ( ) {
<- c
log . Info ( "Shutting down..." )
shutdown ( app )
log . Info ( "Done." )
os . Exit ( 0 )
} ( )
http . Handle ( "/" , r )
2018-11-21 23:26:19 +00:00
// Start web application server
2018-11-26 15:50:36 +00:00
var bindAddress = app . cfg . Server . Bind
if bindAddress == "" {
bindAddress = "localhost"
}
2018-11-21 23:26:19 +00:00
if app . cfg . IsSecureStandalone ( ) {
2018-11-26 17:32:18 +00:00
log . Info ( "Serving redirects on http://%s:80" , bindAddress )
2018-11-21 23:26:19 +00:00
go func ( ) {
2018-11-26 17:32:18 +00:00
err = http . ListenAndServe (
fmt . Sprintf ( "%s:80" , bindAddress ) , http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
http . Redirect ( w , r , app . cfg . App . Host , http . StatusMovedPermanently )
} ) )
2018-11-21 23:26:19 +00:00
log . Error ( "Unable to start redirect server: %v" , err )
} ( )
2018-11-27 15:55:43 +00:00
log . Info ( "Serving on https://%s:443" , bindAddress )
2018-11-21 23:26:19 +00:00
log . Info ( "---" )
2018-11-26 15:50:36 +00:00
err = http . ListenAndServeTLS (
fmt . Sprintf ( "%s:443" , bindAddress ) , app . cfg . Server . TLSCertPath , app . cfg . Server . TLSKeyPath , nil )
2018-11-21 23:26:19 +00:00
} else {
2018-11-26 15:50:36 +00:00
log . Info ( "Serving on http://%s:%d\n" , bindAddress , app . cfg . Server . Port )
2018-11-21 23:26:19 +00:00
log . Info ( "---" )
2018-11-26 15:50:36 +00:00
err = http . ListenAndServe ( fmt . Sprintf ( "%s:%d" , bindAddress , app . cfg . Server . Port ) , nil )
2018-11-21 23:26:19 +00:00
}
2018-11-11 01:41:35 +00:00
if err != nil {
log . Error ( "Unable to start: %v" , err )
os . Exit ( 1 )
}
2018-10-15 18:44:15 +00:00
}
2018-12-08 15:39:25 +00:00
func loadConfig ( app * app ) {
2018-12-08 22:49:19 +00:00
log . Info ( "Loading %s configuration..." , app . cfgFile )
cfg , err := config . Load ( app . cfgFile )
2018-12-08 15:39:25 +00:00
if err != nil {
log . Error ( "Unable to load configuration: %v" , err )
os . Exit ( 1 )
}
app . cfg = cfg
}
2018-11-10 03:16:13 +00:00
func connectToDatabase ( app * app ) {
2018-11-21 18:10:10 +00:00
log . Info ( "Connecting to %s database..." , app . cfg . Database . Type )
2018-11-26 20:11:10 +00:00
2018-12-02 22:21:43 +00:00
var db * sql . DB
var err error
2019-01-05 22:51:17 +00:00
if app . cfg . Database . Type == driverMySQL {
2018-12-02 22:21:43 +00:00
db , err = sql . Open ( app . cfg . Database . Type , fmt . Sprintf ( "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s" , app . cfg . Database . User , app . cfg . Database . Password , app . cfg . Database . Host , app . cfg . Database . Port , app . cfg . Database . Database , url . QueryEscape ( time . Local . String ( ) ) ) )
2018-12-03 14:53:31 +00:00
db . SetMaxOpenConns ( 50 )
2019-01-05 22:51:17 +00:00
} else if app . cfg . Database . Type == driverSQLite {
2019-01-03 22:57:06 +00:00
if ! SQLiteEnabled {
log . Error ( "Invalid database type '%s'. Binary wasn't compiled with SQLite3 support." , app . cfg . Database . Type )
os . Exit ( 1 )
}
2018-12-08 18:34:29 +00:00
if app . cfg . Database . FileName == "" {
log . Error ( "SQLite database filename value in config.ini is empty." )
os . Exit ( 1 )
}
2019-01-07 16:55:23 +00:00
db , err = sql . Open ( "sqlite3_with_regex" , app . cfg . Database . FileName + "?parseTime=true&cached=shared" )
2018-12-03 14:53:31 +00:00
db . SetMaxOpenConns ( 1 )
2018-11-26 20:11:10 +00:00
} else {
log . Error ( "Invalid database type '%s'. Only 'mysql' and 'sqlite3' are supported right now." , app . cfg . Database . Type )
2018-11-10 03:16:13 +00:00
os . Exit ( 1 )
}
2018-12-02 22:21:43 +00:00
if err != nil {
log . Error ( "%s" , err )
os . Exit ( 1 )
}
app . db = & datastore { db , app . cfg . Database . Type }
2018-11-10 03:16:13 +00:00
}
2018-10-15 18:44:15 +00:00
func shutdown ( app * app ) {
2018-10-17 02:31:27 +00:00
log . Info ( "Closing database connection..." )
app . db . Close ( )
2018-10-15 18:44:15 +00:00
}
2018-12-17 23:13:26 +00:00
2019-01-26 16:06:58 +00:00
func adminCreateUser ( app * app , credStr string , isAdmin bool ) error {
2018-12-17 23:13:26 +00:00
// Create an admin user with --create-admin
creds := strings . Split ( credStr , ":" )
if len ( creds ) != 2 {
2019-01-26 16:11:17 +00:00
c := "user"
if isAdmin {
c = "admin"
}
return fmt . Errorf ( "usage: writefreely --create-%s username:password" , c )
2018-12-17 23:13:26 +00:00
}
loadConfig ( app )
connectToDatabase ( app )
defer shutdown ( app )
// Ensure an admin / first user doesn't already exist
2018-12-22 15:54:08 +00:00
firstUser , _ := app . db . GetUserByID ( 1 )
if isAdmin {
// Abort if trying to create admin user, but one already exists
if firstUser != nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Admin user already exists (%s). Create a regular user with: writefreely --create-user" , firstUser . Username )
2018-12-22 15:54:08 +00:00
}
} else {
// Abort if trying to create regular user, but no admin exists yet
if firstUser == nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "No admin user exists yet. Create an admin first with: writefreely --create-admin" )
2018-12-22 15:54:08 +00:00
}
2018-12-17 23:13:26 +00:00
}
// Create the user
username := creds [ 0 ]
password := creds [ 1 ]
2018-12-20 02:26:13 +00:00
// Normalize and validate username
desiredUsername := username
username = getSlug ( username , "" )
usernameDesc := username
if username != desiredUsername {
usernameDesc += " (originally: " + desiredUsername + ")"
}
if ! author . IsValidUsername ( app . cfg , username ) {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Username %s is invalid, reserved, or shorter than configured minimum length (%d characters)." , usernameDesc , app . cfg . App . MinUsernameLen )
2018-12-20 02:26:13 +00:00
}
// Hash the password
2018-12-17 23:13:26 +00:00
hashedPass , err := auth . HashPass ( [ ] byte ( password ) )
if err != nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Unable to hash password: %v" , err )
2018-12-17 23:13:26 +00:00
}
u := & User {
Username : username ,
HashedPass : hashedPass ,
Created : time . Now ( ) . Truncate ( time . Second ) . UTC ( ) ,
}
2018-12-22 15:54:08 +00:00
userType := "user"
if isAdmin {
userType = "admin"
}
log . Info ( "Creating %s %s..." , userType , usernameDesc )
err = app . db . CreateUser ( u , desiredUsername )
2018-12-17 23:13:26 +00:00
if err != nil {
2019-01-26 16:06:58 +00:00
return fmt . Errorf ( "Unable to create user: %s" , err )
2018-12-17 23:13:26 +00:00
}
log . Info ( "Done!" )
2019-01-26 16:06:58 +00:00
return nil
2018-12-17 23:13:26 +00:00
}
2019-01-13 14:08:47 +00:00
2019-01-26 15:52:11 +00:00
func adminInitDatabase ( app * app ) error {
2019-01-13 14:08:47 +00:00
schemaFileName := "schema.sql"
if app . cfg . Database . Type == driverSQLite {
schemaFileName = "sqlite.sql"
}
schema , err := Asset ( schemaFileName )
if err != nil {
2019-01-26 15:52:11 +00:00
return fmt . Errorf ( "Unable to load schema file: %v" , err )
2019-01-13 14:08:47 +00:00
}
tblReg := regexp . MustCompile ( "CREATE TABLE (IF NOT EXISTS )?`([a-z_]+)`" )
queries := strings . Split ( string ( schema ) , ";\n" )
for _ , q := range queries {
if strings . TrimSpace ( q ) == "" {
continue
}
parts := tblReg . FindStringSubmatch ( q )
if len ( parts ) >= 3 {
log . Info ( "Creating table %s..." , parts [ 2 ] )
} else {
log . Info ( "Creating table ??? (Weird query) No match in: %v" , parts )
}
_ , err = app . db . Exec ( q )
if err != nil {
log . Error ( "%s" , err )
} else {
log . Info ( "Created." )
}
}
2019-01-24 22:08:08 +00:00
// Set up migrations table
2019-04-15 17:48:19 +00:00
log . Info ( "Initializing appmigrations table..." )
2019-01-24 22:08:08 +00:00
err = migrations . SetInitialMigrations ( migrations . NewDatastore ( app . db . DB , app . db . driverName ) )
if err != nil {
2019-01-26 15:52:11 +00:00
return fmt . Errorf ( "Unable to set initial migrations: %v" , err )
2019-01-24 22:08:08 +00:00
}
2019-04-15 17:48:19 +00:00
log . Info ( "Running migrations..." )
err = migrations . Migrate ( migrations . NewDatastore ( app . db . DB , app . db . driverName ) )
if err != nil {
return fmt . Errorf ( "migrate: %s" , err )
}
2019-01-26 15:52:11 +00:00
log . Info ( "Done." )
return nil
2019-01-13 14:08:47 +00:00
}