A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

383 lines
9.4 KiB

  1. /*
  2. * Copyright © 2018 Musing Studio LLC.
  3. *
  4. * This file is part of WriteFreely.
  5. *
  6. * WriteFreely is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License, included
  8. * in the LICENSE file in this source code package.
  9. */
  10. package config
  11. import (
  12. "fmt"
  13. "github.com/fatih/color"
  14. "github.com/manifoldco/promptui"
  15. "github.com/mitchellh/go-wordwrap"
  16. "github.com/writeas/web-core/auth"
  17. "strconv"
  18. "strings"
  19. )
  20. type SetupData struct {
  21. User *UserCreation
  22. Config *Config
  23. }
  24. func Configure(fname string, configSections string) (*SetupData, error) {
  25. data := &SetupData{}
  26. var err error
  27. if fname == "" {
  28. fname = FileName
  29. }
  30. data.Config, err = Load(fname)
  31. var action string
  32. isNewCfg := false
  33. if err != nil {
  34. fmt.Printf("No %s configuration yet. Creating new.\n", fname)
  35. data.Config = New()
  36. action = "generate"
  37. isNewCfg = true
  38. } else {
  39. fmt.Printf("Loaded configuration %s.\n", fname)
  40. action = "update"
  41. }
  42. title := color.New(color.Bold, color.BgGreen).PrintFunc()
  43. intro := color.New(color.Bold, color.FgWhite).PrintlnFunc()
  44. fmt.Println()
  45. intro(" ✍ WriteFreely Configuration ✍")
  46. fmt.Println()
  47. fmt.Println(wordwrap.WrapString(" This quick configuration process will "+action+" the application's config file, "+fname+".\n\n It validates your input along the way, so you can be sure any future errors aren't caused by a bad configuration. If you'd rather configure your server manually, instead run: writefreely --create-config and edit that file.", 75))
  48. fmt.Println()
  49. tmpls := &promptui.PromptTemplates{
  50. Success: "{{ . | bold | faint }}: ",
  51. }
  52. selTmpls := &promptui.SelectTemplates{
  53. Selected: fmt.Sprintf(`{{.Label}} {{ . | faint }}`),
  54. }
  55. var selPrompt promptui.Select
  56. var prompt promptui.Prompt
  57. if strings.Contains(configSections, "server") {
  58. title(" Server setup ")
  59. fmt.Println()
  60. // Environment selection
  61. selPrompt = promptui.Select{
  62. Templates: selTmpls,
  63. Label: "Environment",
  64. Items: []string{"Development", "Production, standalone", "Production, behind reverse proxy"},
  65. }
  66. _, envType, err := selPrompt.Run()
  67. if err != nil {
  68. return data, err
  69. }
  70. isDevEnv := envType == "Development"
  71. isStandalone := envType == "Production, standalone"
  72. data.Config.Server.Dev = isDevEnv
  73. if isDevEnv || !isStandalone {
  74. // Running in dev environment or behind reverse proxy; ask for port
  75. prompt = promptui.Prompt{
  76. Templates: tmpls,
  77. Label: "Local port",
  78. Validate: validatePort,
  79. Default: fmt.Sprintf("%d", data.Config.Server.Port),
  80. }
  81. port, err := prompt.Run()
  82. if err != nil {
  83. return data, err
  84. }
  85. data.Config.Server.Port, _ = strconv.Atoi(port) // Ignore error, as we've already validated number
  86. }
  87. if isStandalone {
  88. selPrompt = promptui.Select{
  89. Templates: selTmpls,
  90. Label: "Web server mode",
  91. Items: []string{"Insecure (port 80)", "Secure (port 443), manual certificate", "Secure (port 443), auto certificate"},
  92. }
  93. sel, _, err := selPrompt.Run()
  94. if err != nil {
  95. return data, err
  96. }
  97. if sel == 0 {
  98. data.Config.Server.Autocert = false
  99. data.Config.Server.Port = 80
  100. data.Config.Server.TLSCertPath = ""
  101. data.Config.Server.TLSKeyPath = ""
  102. } else if sel == 1 || sel == 2 {
  103. data.Config.Server.Port = 443
  104. data.Config.Server.Autocert = sel == 2
  105. if sel == 1 {
  106. // Manual certificate configuration
  107. prompt = promptui.Prompt{
  108. Templates: tmpls,
  109. Label: "Certificate path",
  110. Validate: validateNonEmpty,
  111. Default: data.Config.Server.TLSCertPath,
  112. }
  113. data.Config.Server.TLSCertPath, err = prompt.Run()
  114. if err != nil {
  115. return data, err
  116. }
  117. prompt = promptui.Prompt{
  118. Templates: tmpls,
  119. Label: "Key path",
  120. Validate: validateNonEmpty,
  121. Default: data.Config.Server.TLSKeyPath,
  122. }
  123. data.Config.Server.TLSKeyPath, err = prompt.Run()
  124. if err != nil {
  125. return data, err
  126. }
  127. } else {
  128. // Automatic certificate
  129. data.Config.Server.TLSCertPath = "certs"
  130. data.Config.Server.TLSKeyPath = "certs"
  131. }
  132. }
  133. } else {
  134. data.Config.Server.TLSCertPath = ""
  135. data.Config.Server.TLSKeyPath = ""
  136. }
  137. fmt.Println()
  138. }
  139. if strings.Contains(configSections, "db") {
  140. title(" Database setup ")
  141. fmt.Println()
  142. selPrompt = promptui.Select{
  143. Templates: selTmpls,
  144. Label: "Database driver",
  145. Items: []string{"MySQL", "SQLite"},
  146. }
  147. sel, _, err := selPrompt.Run()
  148. if err != nil {
  149. return data, err
  150. }
  151. if sel == 0 {
  152. // Configure for MySQL
  153. data.Config.UseMySQL(isNewCfg)
  154. prompt = promptui.Prompt{
  155. Templates: tmpls,
  156. Label: "Username",
  157. Validate: validateNonEmpty,
  158. Default: data.Config.Database.User,
  159. }
  160. data.Config.Database.User, err = prompt.Run()
  161. if err != nil {
  162. return data, err
  163. }
  164. prompt = promptui.Prompt{
  165. Templates: tmpls,
  166. Label: "Password",
  167. Validate: validateNonEmpty,
  168. Default: data.Config.Database.Password,
  169. Mask: '*',
  170. }
  171. data.Config.Database.Password, err = prompt.Run()
  172. if err != nil {
  173. return data, err
  174. }
  175. prompt = promptui.Prompt{
  176. Templates: tmpls,
  177. Label: "Database name",
  178. Validate: validateNonEmpty,
  179. Default: data.Config.Database.Database,
  180. }
  181. data.Config.Database.Database, err = prompt.Run()
  182. if err != nil {
  183. return data, err
  184. }
  185. prompt = promptui.Prompt{
  186. Templates: tmpls,
  187. Label: "Host",
  188. Validate: validateNonEmpty,
  189. Default: data.Config.Database.Host,
  190. }
  191. data.Config.Database.Host, err = prompt.Run()
  192. if err != nil {
  193. return data, err
  194. }
  195. prompt = promptui.Prompt{
  196. Templates: tmpls,
  197. Label: "Port",
  198. Validate: validatePort,
  199. Default: fmt.Sprintf("%d", data.Config.Database.Port),
  200. }
  201. dbPort, err := prompt.Run()
  202. if err != nil {
  203. return data, err
  204. }
  205. data.Config.Database.Port, _ = strconv.Atoi(dbPort) // Ignore error, as we've already validated number
  206. } else if sel == 1 {
  207. // Configure for SQLite
  208. data.Config.UseSQLite(isNewCfg)
  209. prompt = promptui.Prompt{
  210. Templates: tmpls,
  211. Label: "Filename",
  212. Validate: validateNonEmpty,
  213. Default: data.Config.Database.FileName,
  214. }
  215. data.Config.Database.FileName, err = prompt.Run()
  216. if err != nil {
  217. return data, err
  218. }
  219. }
  220. fmt.Println()
  221. }
  222. if strings.Contains(configSections, "app") {
  223. title(" App setup ")
  224. fmt.Println()
  225. selPrompt = promptui.Select{
  226. Templates: selTmpls,
  227. Label: "Site type",
  228. Items: []string{"Single user blog", "Multi-user instance"},
  229. }
  230. _, usersType, err := selPrompt.Run()
  231. if err != nil {
  232. return data, err
  233. }
  234. data.Config.App.SingleUser = usersType == "Single user blog"
  235. if data.Config.App.SingleUser {
  236. data.User = &UserCreation{}
  237. // prompt for username
  238. prompt = promptui.Prompt{
  239. Templates: tmpls,
  240. Label: "Admin username",
  241. Validate: validateNonEmpty,
  242. }
  243. data.User.Username, err = prompt.Run()
  244. if err != nil {
  245. return data, err
  246. }
  247. // prompt for password
  248. prompt = promptui.Prompt{
  249. Templates: tmpls,
  250. Label: "Admin password",
  251. Validate: validateNonEmpty,
  252. }
  253. newUserPass, err := prompt.Run()
  254. if err != nil {
  255. return data, err
  256. }
  257. data.User.HashedPass, err = auth.HashPass([]byte(newUserPass))
  258. if err != nil {
  259. return data, err
  260. }
  261. }
  262. siteNameLabel := "Instance name"
  263. if data.Config.App.SingleUser {
  264. siteNameLabel = "Blog name"
  265. }
  266. prompt = promptui.Prompt{
  267. Templates: tmpls,
  268. Label: siteNameLabel,
  269. Validate: validateNonEmpty,
  270. Default: data.Config.App.SiteName,
  271. }
  272. data.Config.App.SiteName, err = prompt.Run()
  273. if err != nil {
  274. return data, err
  275. }
  276. prompt = promptui.Prompt{
  277. Templates: tmpls,
  278. Label: "Public URL",
  279. Validate: validateDomain,
  280. Default: data.Config.App.Host,
  281. }
  282. data.Config.App.Host, err = prompt.Run()
  283. if err != nil {
  284. return data, err
  285. }
  286. if !data.Config.App.SingleUser {
  287. selPrompt = promptui.Select{
  288. Templates: selTmpls,
  289. Label: "Registration",
  290. Items: []string{"Open", "Closed"},
  291. }
  292. _, regType, err := selPrompt.Run()
  293. if err != nil {
  294. return data, err
  295. }
  296. data.Config.App.OpenRegistration = regType == "Open"
  297. prompt = promptui.Prompt{
  298. Templates: tmpls,
  299. Label: "Max blogs per user",
  300. Default: fmt.Sprintf("%d", data.Config.App.MaxBlogs),
  301. }
  302. maxBlogs, err := prompt.Run()
  303. if err != nil {
  304. return data, err
  305. }
  306. data.Config.App.MaxBlogs, _ = strconv.Atoi(maxBlogs) // Ignore error, as we've already validated number
  307. }
  308. selPrompt = promptui.Select{
  309. Templates: selTmpls,
  310. Label: "Federation",
  311. Items: []string{"Enabled", "Disabled"},
  312. }
  313. _, fedType, err := selPrompt.Run()
  314. if err != nil {
  315. return data, err
  316. }
  317. data.Config.App.Federation = fedType == "Enabled"
  318. if data.Config.App.Federation {
  319. selPrompt = promptui.Select{
  320. Templates: selTmpls,
  321. Label: "Usage stats (active users, posts)",
  322. Items: []string{"Public", "Private"},
  323. }
  324. _, fedStatsType, err := selPrompt.Run()
  325. if err != nil {
  326. return data, err
  327. }
  328. data.Config.App.PublicStats = fedStatsType == "Public"
  329. selPrompt = promptui.Select{
  330. Templates: selTmpls,
  331. Label: "Instance metadata privacy",
  332. Items: []string{"Public", "Private"},
  333. }
  334. _, fedStatsType, err = selPrompt.Run()
  335. if err != nil {
  336. return data, err
  337. }
  338. data.Config.App.Private = fedStatsType == "Private"
  339. }
  340. }
  341. return data, Save(data.Config, fname)
  342. }