Added basic auth system, profiles, and start of models and data.
This commit is contained in:
parent
a24ab226c2
commit
e198f45b92
|
@ -14,3 +14,5 @@ finnow-api-test-*
|
||||||
*.o
|
*.o
|
||||||
*.obj
|
*.obj
|
||||||
*.lst
|
*.lst
|
||||||
|
|
||||||
|
users/
|
||||||
|
|
|
@ -0,0 +1,442 @@
|
||||||
|
Entity,Currency,AlphabeticCode,NumericCode,MinorUnit,WithdrawalDate
|
||||||
|
AFGHANISTAN,Afghani,AFN,971,2,
|
||||||
|
ÅLAND ISLANDS,Euro,EUR,978,2,
|
||||||
|
ALBANIA,Lek,ALL,008,2,
|
||||||
|
ALGERIA,Algerian Dinar,DZD,012,2,
|
||||||
|
AMERICAN SAMOA,US Dollar,USD,840,2,
|
||||||
|
ANDORRA,Euro,EUR,978,2,
|
||||||
|
ANGOLA,Kwanza,AOA,973,2,
|
||||||
|
ANGUILLA,East Caribbean Dollar,XCD,951,2,
|
||||||
|
ANTARCTICA,No universal currency,,,,
|
||||||
|
ANTIGUA AND BARBUDA,East Caribbean Dollar,XCD,951,2,
|
||||||
|
ARGENTINA,Argentine Peso,ARS,032,2,
|
||||||
|
ARMENIA,Armenian Dram,AMD,051,2,
|
||||||
|
ARUBA,Aruban Florin,AWG,533,2,
|
||||||
|
AUSTRALIA,Australian Dollar,AUD,036,2,
|
||||||
|
AUSTRIA,Euro,EUR,978,2,
|
||||||
|
AZERBAIJAN,Azerbaijan Manat,AZN,944,2,
|
||||||
|
BAHAMAS (THE),Bahamian Dollar,BSD,044,2,
|
||||||
|
BAHRAIN,Bahraini Dinar,BHD,048,3,
|
||||||
|
BANGLADESH,Taka,BDT,050,2,
|
||||||
|
BARBADOS,Barbados Dollar,BBD,052,2,
|
||||||
|
BELARUS,Belarusian Ruble,BYN,933,2,
|
||||||
|
BELGIUM,Euro,EUR,978,2,
|
||||||
|
BELIZE,Belize Dollar,BZD,084,2,
|
||||||
|
BENIN,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
BERMUDA,Bermudian Dollar,BMD,060,2,
|
||||||
|
BHUTAN,Indian Rupee,INR,356,2,
|
||||||
|
BHUTAN,Ngultrum,BTN,064,2,
|
||||||
|
BOLIVIA (PLURINATIONAL STATE OF),Boliviano,BOB,068,2,
|
||||||
|
BOLIVIA (PLURINATIONAL STATE OF),Mvdol,BOV,984,2,
|
||||||
|
"BONAIRE, SINT EUSTATIUS AND SABA",US Dollar,USD,840,2,
|
||||||
|
BOSNIA AND HERZEGOVINA,Convertible Mark,BAM,977,2,
|
||||||
|
BOTSWANA,Pula,BWP,072,2,
|
||||||
|
BOUVET ISLAND,Norwegian Krone,NOK,578,2,
|
||||||
|
BRAZIL,Brazilian Real,BRL,986,2,
|
||||||
|
BRITISH INDIAN OCEAN TERRITORY (THE),US Dollar,USD,840,2,
|
||||||
|
BRUNEI DARUSSALAM,Brunei Dollar,BND,096,2,
|
||||||
|
BULGARIA,Bulgarian Lev,BGN,975,2,
|
||||||
|
BURKINA FASO,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
BURUNDI,Burundi Franc,BIF,108,0,
|
||||||
|
CABO VERDE,Cabo Verde Escudo,CVE,132,2,
|
||||||
|
CAMBODIA,Riel,KHR,116,2,
|
||||||
|
CAMEROON,CFA Franc BEAC,XAF,950,0,
|
||||||
|
CANADA,Canadian Dollar,CAD,124,2,
|
||||||
|
CAYMAN ISLANDS (THE),Cayman Islands Dollar,KYD,136,2,
|
||||||
|
CENTRAL AFRICAN REPUBLIC (THE),CFA Franc BEAC,XAF,950,0,
|
||||||
|
CHAD,CFA Franc BEAC,XAF,950,0,
|
||||||
|
CHILE,Chilean Peso,CLP,152,0,
|
||||||
|
CHILE,Unidad de Fomento,CLF,990,4,
|
||||||
|
CHINA,Yuan Renminbi,CNY,156,2,
|
||||||
|
CHRISTMAS ISLAND,Australian Dollar,AUD,036,2,
|
||||||
|
COCOS (KEELING) ISLANDS (THE),Australian Dollar,AUD,036,2,
|
||||||
|
COLOMBIA,Colombian Peso,COP,170,2,
|
||||||
|
COLOMBIA,Unidad de Valor Real,COU,970,2,
|
||||||
|
COMOROS (THE),Comorian Franc ,KMF,174,0,
|
||||||
|
CONGO (THE DEMOCRATIC REPUBLIC OF THE),Congolese Franc,CDF,976,2,
|
||||||
|
CONGO (THE),CFA Franc BEAC,XAF,950,0,
|
||||||
|
COOK ISLANDS (THE),New Zealand Dollar,NZD,554,2,
|
||||||
|
COSTA RICA,Costa Rican Colon,CRC,188,2,
|
||||||
|
CÔTE D'IVOIRE,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
CROATIA,Kuna,HRK,191,2,
|
||||||
|
CUBA,Cuban Peso,CUP,192,2,
|
||||||
|
CUBA,Peso Convertible,CUC,931,2,
|
||||||
|
CURAÇAO,Netherlands Antillean Guilder,ANG,532,2,
|
||||||
|
CYPRUS,Euro,EUR,978,2,
|
||||||
|
CZECHIA,Czech Koruna,CZK,203,2,
|
||||||
|
DENMARK,Danish Krone,DKK,208,2,
|
||||||
|
DJIBOUTI,Djibouti Franc,DJF,262,0,
|
||||||
|
DOMINICA,East Caribbean Dollar,XCD,951,2,
|
||||||
|
DOMINICAN REPUBLIC (THE),Dominican Peso,DOP,214,2,
|
||||||
|
ECUADOR,US Dollar,USD,840,2,
|
||||||
|
EGYPT,Egyptian Pound,EGP,818,2,
|
||||||
|
EL SALVADOR,El Salvador Colon,SVC,222,2,
|
||||||
|
EL SALVADOR,US Dollar,USD,840,2,
|
||||||
|
EQUATORIAL GUINEA,CFA Franc BEAC,XAF,950,0,
|
||||||
|
ERITREA,Nakfa,ERN,232,2,
|
||||||
|
ESTONIA,Euro,EUR,978,2,
|
||||||
|
ESWATINI,Lilangeni,SZL,748,2,
|
||||||
|
ETHIOPIA,Ethiopian Birr,ETB,230,2,
|
||||||
|
EUROPEAN UNION,Euro,EUR,978,2,
|
||||||
|
"FALKLAND ISLANDS (THE) [MALVINAS]",Falkland Islands Pound,FKP,238,2,
|
||||||
|
FAROE ISLANDS (THE),Danish Krone,DKK,208,2,
|
||||||
|
FIJI,Fiji Dollar,FJD,242,2,
|
||||||
|
FINLAND,Euro,EUR,978,2,
|
||||||
|
FRANCE,Euro,EUR,978,2,
|
||||||
|
FRENCH GUIANA,Euro,EUR,978,2,
|
||||||
|
FRENCH POLYNESIA,CFP Franc,XPF,953,0,
|
||||||
|
FRENCH SOUTHERN TERRITORIES (THE),Euro,EUR,978,2,
|
||||||
|
GABON,CFA Franc BEAC,XAF,950,0,
|
||||||
|
GAMBIA (THE),Dalasi,GMD,270,2,
|
||||||
|
GEORGIA,Lari,GEL,981,2,
|
||||||
|
GERMANY,Euro,EUR,978,2,
|
||||||
|
GHANA,Ghana Cedi,GHS,936,2,
|
||||||
|
GIBRALTAR,Gibraltar Pound,GIP,292,2,
|
||||||
|
GREECE,Euro,EUR,978,2,
|
||||||
|
GREENLAND,Danish Krone,DKK,208,2,
|
||||||
|
GRENADA,East Caribbean Dollar,XCD,951,2,
|
||||||
|
GUADELOUPE,Euro,EUR,978,2,
|
||||||
|
GUAM,US Dollar,USD,840,2,
|
||||||
|
GUATEMALA,Quetzal,GTQ,320,2,
|
||||||
|
GUERNSEY,Pound Sterling,GBP,826,2,
|
||||||
|
GUINEA,Guinean Franc,GNF,324,0,
|
||||||
|
GUINEA-BISSAU,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
GUYANA,Guyana Dollar,GYD,328,2,
|
||||||
|
HAITI,Gourde,HTG,332,2,
|
||||||
|
HAITI,US Dollar,USD,840,2,
|
||||||
|
HEARD ISLAND AND McDONALD ISLANDS,Australian Dollar,AUD,036,2,
|
||||||
|
HOLY SEE (THE),Euro,EUR,978,2,
|
||||||
|
HONDURAS,Lempira,HNL,340,2,
|
||||||
|
HONG KONG,Hong Kong Dollar,HKD,344,2,
|
||||||
|
HUNGARY,Forint,HUF,348,2,
|
||||||
|
ICELAND,Iceland Krona,ISK,352,0,
|
||||||
|
INDIA,Indian Rupee,INR,356,2,
|
||||||
|
INDONESIA,Rupiah,IDR,360,2,
|
||||||
|
INTERNATIONAL MONETARY FUND (IMF),SDR (Special Drawing Right),XDR,960,-,
|
||||||
|
IRAN (ISLAMIC REPUBLIC OF),Iranian Rial,IRR,364,2,
|
||||||
|
IRAQ,Iraqi Dinar,IQD,368,3,
|
||||||
|
IRELAND,Euro,EUR,978,2,
|
||||||
|
ISLE OF MAN,Pound Sterling,GBP,826,2,
|
||||||
|
ISRAEL,New Israeli Sheqel,ILS,376,2,
|
||||||
|
ITALY,Euro,EUR,978,2,
|
||||||
|
JAMAICA,Jamaican Dollar,JMD,388,2,
|
||||||
|
JAPAN,Yen,JPY,392,0,
|
||||||
|
JERSEY,Pound Sterling,GBP,826,2,
|
||||||
|
JORDAN,Jordanian Dinar,JOD,400,3,
|
||||||
|
KAZAKHSTAN,Tenge,KZT,398,2,
|
||||||
|
KENYA,Kenyan Shilling,KES,404,2,
|
||||||
|
KIRIBATI,Australian Dollar,AUD,036,2,
|
||||||
|
KOREA (THE DEMOCRATIC PEOPLE'S REPUBLIC OF),North Korean Won,KPW,408,2,
|
||||||
|
KOREA (THE REPUBLIC OF),Won,KRW,410,0,
|
||||||
|
KUWAIT,Kuwaiti Dinar,KWD,414,3,
|
||||||
|
KYRGYZSTAN,Som,KGS,417,2,
|
||||||
|
LAO PEOPLE'S DEMOCRATIC REPUBLIC (THE),Lao Kip,LAK,418,2,
|
||||||
|
LATVIA,Euro,EUR,978,2,
|
||||||
|
LEBANON,Lebanese Pound,LBP,422,2,
|
||||||
|
LESOTHO,Loti,LSL,426,2,
|
||||||
|
LESOTHO,Rand,ZAR,710,2,
|
||||||
|
LIBERIA,Liberian Dollar,LRD,430,2,
|
||||||
|
LIBYA,Libyan Dinar,LYD,434,3,
|
||||||
|
LIECHTENSTEIN,Swiss Franc,CHF,756,2,
|
||||||
|
LITHUANIA,Euro,EUR,978,2,
|
||||||
|
LUXEMBOURG,Euro,EUR,978,2,
|
||||||
|
MACAO,Pataca,MOP,446,2,
|
||||||
|
NORTH MACEDONIA,Denar,MKD,807,2,
|
||||||
|
MADAGASCAR,Malagasy Ariary,MGA,969,2,
|
||||||
|
MALAWI,Malawi Kwacha,MWK,454,2,
|
||||||
|
MALAYSIA,Malaysian Ringgit,MYR,458,2,
|
||||||
|
MALDIVES,Rufiyaa,MVR,462,2,
|
||||||
|
MALI,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
MALTA,Euro,EUR,978,2,
|
||||||
|
MARSHALL ISLANDS (THE),US Dollar,USD,840,2,
|
||||||
|
MARTINIQUE,Euro,EUR,978,2,
|
||||||
|
MAURITANIA,Ouguiya,MRU,929,2,
|
||||||
|
MAURITIUS,Mauritius Rupee,MUR,480,2,
|
||||||
|
MAYOTTE,Euro,EUR,978,2,
|
||||||
|
MEMBER COUNTRIES OF THE AFRICAN DEVELOPMENT BANK GROUP,ADB Unit of Account,XUA,965,-,
|
||||||
|
MEXICO,Mexican Peso,MXN,484,2,
|
||||||
|
MEXICO,Mexican Unidad de Inversion (UDI),MXV,979,2,
|
||||||
|
MICRONESIA (FEDERATED STATES OF),US Dollar,USD,840,2,
|
||||||
|
MOLDOVA (THE REPUBLIC OF),Moldovan Leu,MDL,498,2,
|
||||||
|
MONACO,Euro,EUR,978,2,
|
||||||
|
MONGOLIA,Tugrik,MNT,496,2,
|
||||||
|
MONTENEGRO,Euro,EUR,978,2,
|
||||||
|
MONTSERRAT,East Caribbean Dollar,XCD,951,2,
|
||||||
|
MOROCCO,Moroccan Dirham,MAD,504,2,
|
||||||
|
MOZAMBIQUE,Mozambique Metical,MZN,943,2,
|
||||||
|
MYANMAR,Kyat,MMK,104,2,
|
||||||
|
NAMIBIA,Namibia Dollar,NAD,516,2,
|
||||||
|
NAMIBIA,Rand,ZAR,710,2,
|
||||||
|
NAURU,Australian Dollar,AUD,036,2,
|
||||||
|
NEPAL,Nepalese Rupee,NPR,524,2,
|
||||||
|
NETHERLANDS (THE),Euro,EUR,978,2,
|
||||||
|
NEW CALEDONIA,CFP Franc,XPF,953,0,
|
||||||
|
NEW ZEALAND,New Zealand Dollar,NZD,554,2,
|
||||||
|
NICARAGUA,Cordoba Oro,NIO,558,2,
|
||||||
|
NIGER (THE),CFA Franc BCEAO,XOF,952,0,
|
||||||
|
NIGERIA,Naira,NGN,566,2,
|
||||||
|
NIUE,New Zealand Dollar,NZD,554,2,
|
||||||
|
NORFOLK ISLAND,Australian Dollar,AUD,036,2,
|
||||||
|
NORTHERN MARIANA ISLANDS (THE),US Dollar,USD,840,2,
|
||||||
|
NORWAY,Norwegian Krone,NOK,578,2,
|
||||||
|
OMAN,Rial Omani,OMR,512,3,
|
||||||
|
PAKISTAN,Pakistan Rupee,PKR,586,2,
|
||||||
|
PALAU,US Dollar,USD,840,2,
|
||||||
|
"PALESTINE, STATE OF",No universal currency,,,,
|
||||||
|
PANAMA,Balboa,PAB,590,2,
|
||||||
|
PANAMA,US Dollar,USD,840,2,
|
||||||
|
PAPUA NEW GUINEA,Kina,PGK,598,2,
|
||||||
|
PARAGUAY,Guarani,PYG,600,0,
|
||||||
|
PERU,Sol,PEN,604,2,
|
||||||
|
PHILIPPINES (THE),Philippine Peso,PHP,608,2,
|
||||||
|
PITCAIRN,New Zealand Dollar,NZD,554,2,
|
||||||
|
POLAND,Zloty,PLN,985,2,
|
||||||
|
PORTUGAL,Euro,EUR,978,2,
|
||||||
|
PUERTO RICO,US Dollar,USD,840,2,
|
||||||
|
QATAR,Qatari Rial,QAR,634,2,
|
||||||
|
RÉUNION,Euro,EUR,978,2,
|
||||||
|
ROMANIA,Romanian Leu,RON,946,2,
|
||||||
|
RUSSIAN FEDERATION (THE),Russian Ruble,RUB,643,2,
|
||||||
|
RWANDA,Rwanda Franc,RWF,646,0,
|
||||||
|
SAINT BARTHÉLEMY,Euro,EUR,978,2,
|
||||||
|
"SAINT HELENA, ASCENSION AND TRISTAN DA CUNHA",Saint Helena Pound,SHP,654,2,
|
||||||
|
SAINT KITTS AND NEVIS,East Caribbean Dollar,XCD,951,2,
|
||||||
|
SAINT LUCIA,East Caribbean Dollar,XCD,951,2,
|
||||||
|
SAINT MARTIN (FRENCH PART),Euro,EUR,978,2,
|
||||||
|
SAINT PIERRE AND MIQUELON,Euro,EUR,978,2,
|
||||||
|
SAINT VINCENT AND THE GRENADINES,East Caribbean Dollar,XCD,951,2,
|
||||||
|
SAMOA,Tala,WST,882,2,
|
||||||
|
SAN MARINO,Euro,EUR,978,2,
|
||||||
|
SAO TOME AND PRINCIPE,Dobra,STN,930,2,
|
||||||
|
SAUDI ARABIA,Saudi Riyal,SAR,682,2,
|
||||||
|
SENEGAL,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
SERBIA,Serbian Dinar,RSD,941,2,
|
||||||
|
SEYCHELLES,Seychelles Rupee,SCR,690,2,
|
||||||
|
SIERRA LEONE,Leone,SLL,694,2,
|
||||||
|
SINGAPORE,Singapore Dollar,SGD,702,2,
|
||||||
|
SINT MAARTEN (DUTCH PART),Netherlands Antillean Guilder,ANG,532,2,
|
||||||
|
"SISTEMA UNITARIO DE COMPENSACION REGIONAL DE PAGOS ""SUCRE""",Sucre,XSU,994,-,
|
||||||
|
SLOVAKIA,Euro,EUR,978,2,
|
||||||
|
SLOVENIA,Euro,EUR,978,2,
|
||||||
|
SOLOMON ISLANDS,Solomon Islands Dollar,SBD,090,2,
|
||||||
|
SOMALIA,Somali Shilling,SOS,706,2,
|
||||||
|
SOUTH AFRICA,Rand,ZAR,710,2,
|
||||||
|
SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS,No universal currency,,,,
|
||||||
|
SOUTH SUDAN,South Sudanese Pound,SSP,728,2,
|
||||||
|
SPAIN,Euro,EUR,978,2,
|
||||||
|
SRI LANKA,Sri Lanka Rupee,LKR,144,2,
|
||||||
|
SUDAN (THE),Sudanese Pound,SDG,938,2,
|
||||||
|
SURINAME,Surinam Dollar,SRD,968,2,
|
||||||
|
SVALBARD AND JAN MAYEN,Norwegian Krone,NOK,578,2,
|
||||||
|
SWEDEN,Swedish Krona,SEK,752,2,
|
||||||
|
SWITZERLAND,Swiss Franc,CHF,756,2,
|
||||||
|
SWITZERLAND,WIR Euro,CHE,947,2,
|
||||||
|
SWITZERLAND,WIR Franc,CHW,948,2,
|
||||||
|
SYRIAN ARAB REPUBLIC,Syrian Pound,SYP,760,2,
|
||||||
|
TAIWAN (PROVINCE OF CHINA),New Taiwan Dollar,TWD,901,2,
|
||||||
|
TAJIKISTAN,Somoni,TJS,972,2,
|
||||||
|
"TANZANIA, UNITED REPUBLIC OF",Tanzanian Shilling,TZS,834,2,
|
||||||
|
THAILAND,Baht,THB,764,2,
|
||||||
|
TIMOR-LESTE,US Dollar,USD,840,2,
|
||||||
|
TOGO,CFA Franc BCEAO,XOF,952,0,
|
||||||
|
TOKELAU,New Zealand Dollar,NZD,554,2,
|
||||||
|
TONGA,Pa'anga,TOP,776,2,
|
||||||
|
TRINIDAD AND TOBAGO,Trinidad and Tobago Dollar,TTD,780,2,
|
||||||
|
TUNISIA,Tunisian Dinar,TND,788,3,
|
||||||
|
TURKEY,Turkish Lira,TRY,949,2,
|
||||||
|
TURKMENISTAN,Turkmenistan New Manat,TMT,934,2,
|
||||||
|
TURKS AND CAICOS ISLANDS (THE),US Dollar,USD,840,2,
|
||||||
|
TUVALU,Australian Dollar,AUD,036,2,
|
||||||
|
UGANDA,Uganda Shilling,UGX,800,0,
|
||||||
|
UKRAINE,Hryvnia,UAH,980,2,
|
||||||
|
UNITED ARAB EMIRATES (THE),UAE Dirham,AED,784,2,
|
||||||
|
UNITED KINGDOM OF GREAT BRITAIN AND NORTHERN IRELAND (THE),Pound Sterling,GBP,826,2,
|
||||||
|
UNITED STATES MINOR OUTLYING ISLANDS (THE),US Dollar,USD,840,2,
|
||||||
|
UNITED STATES OF AMERICA (THE),US Dollar,USD,840,2,
|
||||||
|
UNITED STATES OF AMERICA (THE),US Dollar (Next day),USN,997,2,
|
||||||
|
URUGUAY,Peso Uruguayo,UYU,858,2,
|
||||||
|
URUGUAY,Uruguay Peso en Unidades Indexadas (UI),UYI,940,0,
|
||||||
|
URUGUAY,Unidad Previsional,UYW,927,4,
|
||||||
|
UZBEKISTAN,Uzbekistan Sum,UZS,860,2,
|
||||||
|
VANUATU,Vatu,VUV,548,0,
|
||||||
|
VENEZUELA (BOLIVARIAN REPUBLIC OF),Bolívar Soberano,VES,928,2,
|
||||||
|
VIET NAM,Dong,VND,704,0,
|
||||||
|
VIRGIN ISLANDS (BRITISH),US Dollar,USD,840,2,
|
||||||
|
VIRGIN ISLANDS (U.S.),US Dollar,USD,840,2,
|
||||||
|
WALLIS AND FUTUNA,CFP Franc,XPF,953,0,
|
||||||
|
WESTERN SAHARA,Moroccan Dirham,MAD,504,2,
|
||||||
|
YEMEN,Yemeni Rial,YER,886,2,
|
||||||
|
ZAMBIA,Zambian Kwacha,ZMW,967,2,
|
||||||
|
ZIMBABWE,Zimbabwe Dollar,ZWL,932,2,
|
||||||
|
ZZ01_Bond Markets Unit European_EURCO,Bond Markets Unit European Composite Unit (EURCO),XBA,955,-,
|
||||||
|
ZZ02_Bond Markets Unit European_EMU-6,Bond Markets Unit European Monetary Unit (E.M.U.-6),XBB,956,-,
|
||||||
|
ZZ03_Bond Markets Unit European_EUA-9,Bond Markets Unit European Unit of Account 9 (E.U.A.-9),XBC,957,-,
|
||||||
|
ZZ04_Bond Markets Unit European_EUA-17,Bond Markets Unit European Unit of Account 17 (E.U.A.-17),XBD,958,-,
|
||||||
|
ZZ06_Testing_Code,Codes specifically reserved for testing purposes,XTS,963,-,
|
||||||
|
ZZ07_No_Currency,The codes assigned for transactions where no currency is involved,XXX,999,-,
|
||||||
|
ZZ08_Gold,Gold,XAU,959,-,
|
||||||
|
ZZ09_Palladium,Palladium,XPD,964,-,
|
||||||
|
ZZ10_Platinum,Platinum,XPT,962,-,
|
||||||
|
ZZ11_Silver,Silver,XAG,961,-,
|
||||||
|
AFGHANISTAN,Afghani,AFA,004,,2003-01
|
||||||
|
ÅLAND ISLANDS,Markka,FIM,246,,2002-03
|
||||||
|
ALBANIA,Old Lek,ALK,008,,1989-12
|
||||||
|
ANDORRA,Andorran Peseta,ADP,020,,2003-07
|
||||||
|
ANDORRA,Spanish Peseta,ESP,724,,2002-03
|
||||||
|
ANDORRA,French Franc,FRF,250,,2002-03
|
||||||
|
ANGOLA,Kwanza,AOK,024,,1991-03
|
||||||
|
ANGOLA,New Kwanza,AON,024,,2000-02
|
||||||
|
ANGOLA,Kwanza Reajustado,AOR,982,,2000-02
|
||||||
|
ARGENTINA,Austral,ARA,032,,1992-01
|
||||||
|
ARGENTINA,Peso Argentino,ARP,032,,1985-07
|
||||||
|
ARGENTINA,Peso,ARY,032,,1989 to 1990
|
||||||
|
ARMENIA,Russian Ruble,RUR,810,,1994-08
|
||||||
|
AUSTRIA,Schilling,ATS,040,,2002-03
|
||||||
|
AZERBAIJAN,Azerbaijan Manat,AYM,945,,2005-10
|
||||||
|
AZERBAIJAN,Azerbaijanian Manat,AZM,031,,2005-12
|
||||||
|
AZERBAIJAN,Russian Ruble,RUR,810,,1994-08
|
||||||
|
BELARUS,Belarusian Ruble,BYB,112,,2001-01
|
||||||
|
BELARUS,Belarusian Ruble,BYR,974,,2017-01
|
||||||
|
BELARUS,Russian Ruble,RUR,810,,1994-06
|
||||||
|
BELGIUM,Convertible Franc,BEC,993,,1990-03
|
||||||
|
BELGIUM,Belgian Franc,BEF,056,,2002-03
|
||||||
|
BELGIUM,Financial Franc,BEL,992,,1990-03
|
||||||
|
BOLIVIA,Peso boliviano,BOP,068,,1987-02
|
||||||
|
BOSNIA AND HERZEGOVINA,Dinar,BAD,070,,1998-07
|
||||||
|
BRAZIL,Cruzeiro,BRB,076,,1986-03
|
||||||
|
BRAZIL,Cruzado,BRC,076,,1989-02
|
||||||
|
BRAZIL,Cruzeiro,BRE,076,,1993-03
|
||||||
|
BRAZIL,New Cruzado,BRN,076,,1990-03
|
||||||
|
BRAZIL,Cruzeiro Real,BRR,987,,1994-07
|
||||||
|
BULGARIA,Lev A/52,BGJ,100,,1989 to 1990
|
||||||
|
BULGARIA,Lev A/62,BGK,100,,1989 to 1990
|
||||||
|
BULGARIA,Lev,BGL,100,,2003-11
|
||||||
|
BURMA,Kyat,BUK,104,,1990-02
|
||||||
|
CROATIA,Croatian Dinar,HRD,191,,1995-01
|
||||||
|
CROATIA,Croatian Kuna,HRK,191,,2015-06
|
||||||
|
CYPRUS,Cyprus Pound,CYP,196,,2008-01
|
||||||
|
CZECHOSLOVAKIA,Krona A/53,CSJ,203,,1989 to 1990
|
||||||
|
CZECHOSLOVAKIA,Koruna,CSK,200,,1993-03
|
||||||
|
ECUADOR,Sucre,ECS,218,,2000-09
|
||||||
|
ECUADOR,Unidad de Valor Constante (UVC),ECV,983,,2000-09
|
||||||
|
EQUATORIAL GUINEA,Ekwele,GQE,226,,1986-06
|
||||||
|
ESTONIA,Kroon,EEK,233,,2011-01
|
||||||
|
EUROPEAN MONETARY CO-OPERATION FUND (EMCF),European Currency Unit (E.C.U),XEU,954,,1999-01
|
||||||
|
FINLAND,Markka,FIM,246,,2002-03
|
||||||
|
FRANCE,French Franc,FRF,250,,2002-03
|
||||||
|
FRENCH GUIANA,French Franc,FRF,250,,2002-03
|
||||||
|
FRENCH SOUTHERN TERRITORIES,French Franc,FRF,250,,2002-03
|
||||||
|
GEORGIA,Georgian Coupon,GEK,268,,1995-10
|
||||||
|
GEORGIA,Russian Ruble,RUR,810,,1994-04
|
||||||
|
GERMAN DEMOCRATIC REPUBLIC,Mark der DDR,DDM,278,,1990-07 to 1990-09
|
||||||
|
GERMANY,Deutsche Mark,DEM,276,,2002-03
|
||||||
|
GHANA,Cedi,GHC,288,,2008-01
|
||||||
|
GHANA,Ghana Cedi,GHP,939,,2007-06
|
||||||
|
GREECE,Drachma,GRD,300,,2002-03
|
||||||
|
GUADELOUPE,French Franc,FRF,250,,2002-03
|
||||||
|
GUINEA,Syli,GNE,324,,1989-12
|
||||||
|
GUINEA,Syli,GNS,324,,1986-02
|
||||||
|
GUINEA-BISSAU,Guinea Escudo,GWE,624,,1978 to 1981
|
||||||
|
GUINEA-BISSAU,Guinea-Bissau Peso,GWP,624,,1997-05
|
||||||
|
HOLY SEE (VATICAN CITY STATE),Italian Lira,ITL,380,,2002-03
|
||||||
|
ICELAND,Old Krona,ISJ,352,,1989 to 1990
|
||||||
|
IRELAND,Irish Pound,IEP,372,,2002-03
|
||||||
|
ISRAEL,Pound,ILP,376,,1978 to 1981
|
||||||
|
ISRAEL,Old Shekel,ILR,376,,1989 to 1990
|
||||||
|
ITALY,Italian Lira,ITL,380,,2002-03
|
||||||
|
KAZAKHSTAN,Russian Ruble,RUR,810,,1994-05
|
||||||
|
KYRGYZSTAN,Russian Ruble,RUR,810,,1993-01
|
||||||
|
LAO,Pathet Lao Kip,LAJ,418,,1979-12
|
||||||
|
LATVIA,Latvian Lats,LVL,428,,2014-01
|
||||||
|
LATVIA,Latvian Ruble,LVR,428,,1994-12
|
||||||
|
LESOTHO,Loti,LSM,426,,1985-05
|
||||||
|
LESOTHO,Financial Rand,ZAL,991,,1995-03
|
||||||
|
LITHUANIA,Lithuanian Litas,LTL,440,,2014-12
|
||||||
|
LITHUANIA,Talonas,LTT,440,,1993-07
|
||||||
|
LUXEMBOURG,Luxembourg Convertible Franc,LUC,989,,1990-03
|
||||||
|
LUXEMBOURG,Luxembourg Franc,LUF,442,,2002-03
|
||||||
|
LUXEMBOURG,Luxembourg Financial Franc,LUL,988,,1990-03
|
||||||
|
MADAGASCAR,Malagasy Franc,MGF,450,,2004-12
|
||||||
|
MALAWI,Kwacha,MWK,454,,2016-02
|
||||||
|
MALDIVES,Maldive Rupee,MVQ,462,,1989-12
|
||||||
|
MALI,Mali Franc,MLF,466,,1984-11
|
||||||
|
MALTA,Maltese Lira,MTL,470,,2008-01
|
||||||
|
MALTA,Maltese Pound,MTP,470,,1983-06
|
||||||
|
MARTINIQUE,French Franc,FRF,250,,2002-03
|
||||||
|
MAURITANIA,Ouguiya,MRO,478,,2017-12
|
||||||
|
MAYOTTE,French Franc,FRF,250,,2002-03
|
||||||
|
MEXICO,Mexican Peso,MXP,484,,1993-01
|
||||||
|
"MOLDOVA, REPUBLIC OF",Russian Ruble,RUR,810,,1993-12
|
||||||
|
MONACO,French Franc,FRF,250,,2002-03
|
||||||
|
MOZAMBIQUE,Mozambique Escudo,MZE,508,,1978 to 1981
|
||||||
|
MOZAMBIQUE,Mozambique Metical,MZM,508,,2006-06
|
||||||
|
NETHERLANDS,Netherlands Guilder,NLG,528,,2002-03
|
||||||
|
NETHERLANDS ANTILLES,Netherlands Antillean Guilder,ANG,532,,2010-10
|
||||||
|
NICARAGUA,Cordoba,NIC,558,,1990-10
|
||||||
|
PERU,Sol,PEH,604,,1989 to 1990
|
||||||
|
PERU,Inti,PEI,604,,1991-07
|
||||||
|
PERU,Nuevo Sol ,PEN,604,,2015-12
|
||||||
|
PERU,Sol,PES,604,,1986-02
|
||||||
|
POLAND,Zloty,PLZ,616,,1997-01
|
||||||
|
PORTUGAL,Portuguese Escudo,PTE,620,,2002-03
|
||||||
|
RÉUNION,French Franc,FRF,250,,2002-03
|
||||||
|
ROMANIA,Leu A/52,ROK,642,,1989 to 1990
|
||||||
|
ROMANIA,Old Leu,ROL,642,,2005-06
|
||||||
|
ROMANIA,New Romanian Leu ,RON,946,,2015-06
|
||||||
|
RUSSIAN FEDERATION,Russian Ruble,RUR,810,,2004-01
|
||||||
|
SAINT MARTIN,French Franc,FRF,250,,1999-01
|
||||||
|
SAINT PIERRE AND MIQUELON,French Franc,FRF,250,,2002-03
|
||||||
|
SAINT-BARTHÉLEMY,French Franc,FRF,250,,1999-01
|
||||||
|
SAN MARINO,Italian Lira,ITL,380,,2002-03
|
||||||
|
SAO TOME AND PRINCIPE,Dobra,STD,678,,2017-12
|
||||||
|
SERBIA AND MONTENEGRO,Serbian Dinar,CSD,891,,2006-10
|
||||||
|
SERBIA AND MONTENEGRO,Euro,EUR,978,,2006-10
|
||||||
|
SLOVAKIA,Slovak Koruna,SKK,703,,2009-01
|
||||||
|
SLOVENIA,Tolar,SIT,705,,2007-01
|
||||||
|
SOUTH AFRICA,Financial Rand,ZAL,991,,1995-03
|
||||||
|
SOUTH SUDAN,Sudanese Pound,SDG,938,,2012-09
|
||||||
|
SOUTHERN RHODESIA,Rhodesian Dollar,RHD,716,,1978 to 1981
|
||||||
|
SPAIN,Spanish Peseta,ESA,996,,1978 to 1981
|
||||||
|
SPAIN,"""A"" Account (convertible Peseta Account)",ESB,995,,1994-12
|
||||||
|
SPAIN,Spanish Peseta,ESP,724,,2002-03
|
||||||
|
SUDAN,Sudanese Dinar,SDD,736,,2007-07
|
||||||
|
SUDAN,Sudanese Pound,SDP,736,,1998-06
|
||||||
|
SURINAME,Surinam Guilder,SRG,740,,2003-12
|
||||||
|
SWAZILAND,Lilangeni,SZL,748,,2018-08
|
||||||
|
SWITZERLAND,WIR Franc (for electronic),CHC,948,,2004-11
|
||||||
|
TAJIKISTAN,Russian Ruble,RUR,810,,1995-05
|
||||||
|
TAJIKISTAN,Tajik Ruble,TJR,762,,2001-04
|
||||||
|
TIMOR-LESTE,Rupiah,IDR,360,,2002-07
|
||||||
|
TIMOR-LESTE,Timor Escudo,TPE,626,,2002-11
|
||||||
|
TURKEY,Old Turkish Lira,TRL,792,,2005-12
|
||||||
|
TURKEY,New Turkish Lira,TRY,949,,2009-01
|
||||||
|
TURKMENISTAN,Russian Ruble,RUR,810,,1993-10
|
||||||
|
TURKMENISTAN,Turkmenistan Manat,TMM,795,,2009-01
|
||||||
|
UGANDA,Uganda Shilling,UGS,800,,1987-05
|
||||||
|
UGANDA,Old Shilling,UGW,800,,1989 to 1990
|
||||||
|
UKRAINE,Karbovanet,UAK,804,,1996-09
|
||||||
|
UNION OF SOVIET SOCIALIST REPUBLICS,Rouble,SUR,810,,1990-12
|
||||||
|
UNITED STATES,US Dollar (Same day),USS,998,,2014-03
|
||||||
|
URUGUAY,Old Uruguay Peso,UYN,858,,1989-12
|
||||||
|
URUGUAY,Uruguayan Peso,UYP,858,,1993-03
|
||||||
|
UZBEKISTAN,Russian Ruble,RUR,810,,1994-07
|
||||||
|
VENEZUELA,Bolivar,VEB,862,,2008-01
|
||||||
|
VENEZUELA,Bolivar Fuerte,VEF,937,,2011-12
|
||||||
|
VENEZUELA (BOLIVARIAN REPUBLIC OF),Bolivar,VEF,937,,2016-02
|
||||||
|
VENEZUELA (BOLIVARIAN REPUBLIC OF),Bolívar,VEF,937,,2018-08
|
||||||
|
VIETNAM,Old Dong,VNC,704,,1989-1990
|
||||||
|
"YEMEN, DEMOCRATIC",Yemeni Dinar,YDD,720,,1991-09
|
||||||
|
YUGOSLAVIA,New Yugoslavian Dinar,YUD,890,,1990-01
|
||||||
|
YUGOSLAVIA,New Dinar,YUM,891,,2003-07
|
||||||
|
YUGOSLAVIA,Yugoslavian Dinar,YUN,890,,1995-11
|
||||||
|
ZAIRE,New Zaire,ZRN,180,,1999-06
|
||||||
|
ZAIRE,Zaire,ZRZ,180,,1994-02
|
||||||
|
ZAMBIA,Zambian Kwacha,ZMK,894,,2012-12
|
||||||
|
ZIMBABWE,Rhodesian Dollar,ZWC,716,,1989-12
|
||||||
|
ZIMBABWE,Zimbabwe Dollar (old),ZWD,716,,2006-08
|
||||||
|
ZIMBABWE,Zimbabwe Dollar,ZWD,716,,2008-08
|
||||||
|
ZIMBABWE,Zimbabwe Dollar (new),ZWN,942,,2006-09
|
||||||
|
ZIMBABWE,Zimbabwe Dollar,ZWR,935,,2009-06
|
||||||
|
ZZ01_Gold-Franc,Gold-Franc,XFO,,,2006-10
|
||||||
|
ZZ02_RINET Funds Code,RINET Funds Code,XRE,,,1999-11
|
||||||
|
ZZ05_UIC-Franc,UIC-Franc,XFU,,,2013-11
|
|
|
@ -4,9 +4,20 @@
|
||||||
],
|
],
|
||||||
"copyright": "Copyright © 2024, Andrew Lalis",
|
"copyright": "Copyright © 2024, Andrew Lalis",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"handy-httpd": "~>8.4.0"
|
"asdf": "~>0.7.17",
|
||||||
|
"botan": "~>1.13.6",
|
||||||
|
"d2sqlite3": "~>1.0.0",
|
||||||
|
"handy-httpd": "~>8.4.0",
|
||||||
|
"jwt": "~>0.4.0",
|
||||||
|
"slf4d": "~>3.0.1"
|
||||||
},
|
},
|
||||||
"description": "Backend API for Finnow.",
|
"description": "Backend API for Finnow.",
|
||||||
"license": "proprietary",
|
"license": "proprietary",
|
||||||
"name": "finnow-api"
|
"name": "finnow-api",
|
||||||
|
"stringImportPaths": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"subConfigurations": {
|
||||||
|
"d2sqlite3": "all-included"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,18 @@
|
||||||
{
|
{
|
||||||
"fileVersion": 1,
|
"fileVersion": 1,
|
||||||
"versions": {
|
"versions": {
|
||||||
|
"asdf": "0.7.17",
|
||||||
|
"botan": "1.13.6",
|
||||||
|
"botan-math": "1.0.4",
|
||||||
|
"d2sqlite3": "1.0.0",
|
||||||
"handy-httpd": "8.4.0",
|
"handy-httpd": "8.4.0",
|
||||||
"httparsed": "1.2.1",
|
"httparsed": "1.2.1",
|
||||||
|
"jwt": "0.4.0",
|
||||||
|
"memutils": "1.0.10",
|
||||||
|
"mir-algorithm": "3.22.1",
|
||||||
|
"mir-core": "1.7.1",
|
||||||
"path-matcher": "1.2.0",
|
"path-matcher": "1.2.0",
|
||||||
|
"silly": "1.1.1",
|
||||||
"slf4d": "3.0.1",
|
"slf4d": "3.0.1",
|
||||||
"streams": "3.5.0"
|
"streams": "3.5.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
-- This schema is included at compile-time into data : SqliteDataSource.
|
||||||
|
|
||||||
|
-- Basic/Utility Entities
|
||||||
|
|
||||||
|
CREATE TABLE profile_property (
|
||||||
|
property TEXT PRIMARY KEY,
|
||||||
|
value TEXT DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE attachment (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
uploaded_at TEXT NOT NULL,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
content_type TEXT NOT NULL,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
content BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Account Entities
|
||||||
|
|
||||||
|
CREATE TABLE account (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
archived BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
number_suffix TEXT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
currency TEXT NOT NULL,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE account_credit_card_properties (
|
||||||
|
account_id INTEGER PRIMARY KEY,
|
||||||
|
credit_limit TEXT,
|
||||||
|
CONSTRAINT fk_account_credit_card_properties_account
|
||||||
|
FOREIGN KEY (account_id) REFERENCES account(id)
|
||||||
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Transaction Entities
|
||||||
|
|
||||||
|
CREATE TABLE transaction_vendor (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE transaction_category (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
parent_id INTEGER,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
color TEXT NOT NULL DEFAULT 'FFFFFF',
|
||||||
|
CONSTRAINT fk_transaction_category_parent
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES transaction_category(id)
|
||||||
|
ON UPDATE CASCADE ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE transaction_tag (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "transaction" (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
timestamp TEXT NOT NULL,
|
||||||
|
added_at TEXT NOT NULL,
|
||||||
|
amount TEXT NOT NULL,
|
||||||
|
currency TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
vendor_id INTEGER,
|
||||||
|
category_id INTEGER,
|
||||||
|
CONSTRAINT fk_transaction_vendor
|
||||||
|
FOREIGN KEY (vendor_id) REFERENCES transaction_vendor(id)
|
||||||
|
ON UPDATE CASCADE ON DELETE SET NULL,
|
||||||
|
CONSTRAINT fk_transaction_category
|
||||||
|
FOREIGN KEY (category_id) REFERENCES transaction_category(id)
|
||||||
|
ON UPDATE CASCADE ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,50 @@
|
||||||
import slf4d;
|
import slf4d;
|
||||||
import handy_httpd;
|
import handy_httpd;
|
||||||
import handy_httpd.handlers.path_handler;
|
import handy_httpd.handlers.path_handler;
|
||||||
|
import handy_httpd.handlers.filtered_handler;
|
||||||
import model.base;
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
ServerConfig cfg;
|
ServerConfig cfg;
|
||||||
cfg.workerPoolSize = 5;
|
cfg.workerPoolSize = 5;
|
||||||
cfg.port = 8080;
|
cfg.port = 8080;
|
||||||
PathHandler pathHandler = new PathHandler();
|
HttpServer server = new HttpServer(buildHandlers(), cfg);
|
||||||
pathHandler.addMapping(Method.GET, "/status", (ref ctx) {
|
|
||||||
ctx.response.writeBodyString("online");
|
|
||||||
});
|
|
||||||
HttpServer server = new HttpServer(pathHandler, cfg);
|
|
||||||
server.start();
|
server.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PathHandler buildHandlers() {
|
||||||
|
import profile;
|
||||||
|
|
||||||
|
const API_PATH = "/api";
|
||||||
|
PathHandler pathHandler = new PathHandler();
|
||||||
|
|
||||||
|
// Generic, public endpoints:
|
||||||
|
pathHandler.addMapping(Method.GET, API_PATH ~ "/status", (ref ctx) {
|
||||||
|
ctx.response.writeBodyString("online");
|
||||||
|
});
|
||||||
|
pathHandler.addMapping(Method.OPTIONS, API_PATH ~ "/**", (ref ctx) {});
|
||||||
|
|
||||||
|
// Auth Entrypoints:
|
||||||
|
import auth.api;
|
||||||
|
import auth.service;
|
||||||
|
pathHandler.addMapping(Method.POST, API_PATH ~ "/login", &postLogin);
|
||||||
|
pathHandler.addMapping(Method.POST, API_PATH ~ "/register", &postRegister);
|
||||||
|
|
||||||
|
// Authenticated endpoints:
|
||||||
|
PathHandler a = new PathHandler();
|
||||||
|
a.addMapping(Method.GET, API_PATH ~ "/me", &getMyUser);
|
||||||
|
a.addMapping(Method.GET, API_PATH ~ "/profiles", &handleGetProfiles);
|
||||||
|
a.addMapping(Method.POST, API_PATH ~ "/profiles", &handleCreateNewProfile);
|
||||||
|
a.addMapping(Method.DELETE, API_PATH ~ "/profiles/:name", &handleDeleteProfile);
|
||||||
|
a.addMapping(Method.GET, API_PATH ~ "/profiles/:profile/properties", &handleGetProperties);
|
||||||
|
a.addMapping(Method.GET, API_PATH ~ "/profiles/:profile/accounts", (ref ctx) {
|
||||||
|
ctx.response.writeBodyString("your accounts!");
|
||||||
|
});
|
||||||
|
|
||||||
|
HttpRequestFilter tokenAuthenticationFilter = new TokenAuthenticationFilter(SECRET);
|
||||||
|
pathHandler.addMapping(API_PATH ~ "/**", new FilteredRequestHandler(
|
||||||
|
a,
|
||||||
|
[tokenAuthenticationFilter]
|
||||||
|
));
|
||||||
|
|
||||||
|
return pathHandler;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/// API endpoints for authentication-related functions, like registration and login.
|
||||||
|
module auth.api;
|
||||||
|
|
||||||
|
import handy_httpd;
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
import slf4d;
|
||||||
|
|
||||||
|
import auth.model;
|
||||||
|
import auth.dao;
|
||||||
|
import auth.dto;
|
||||||
|
import auth.service;
|
||||||
|
|
||||||
|
void postLogin(ref HttpRequestContext ctx) {
|
||||||
|
LoginCredentials loginCredentials;
|
||||||
|
try {
|
||||||
|
loginCredentials = LoginCredentials.parse(ctx.request.readBodyAsJson());
|
||||||
|
} catch (Exception e) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
}
|
||||||
|
if (!validateUsername(loginCredentials.username)) {
|
||||||
|
ctx.response.status = HttpStatus.UNAUTHORIZED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UserRepository userRepo = new FileSystemUserRepository();
|
||||||
|
Optional!User optionalUser = userRepo.findByUsername(loginCredentials.username);
|
||||||
|
if (optionalUser.isNull) {
|
||||||
|
ctx.response.status = HttpStatus.UNAUTHORIZED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
import botan.passhash.bcrypt : checkBcrypt;
|
||||||
|
if (!checkBcrypt(loginCredentials.password, optionalUser.value.passwordHash)) {
|
||||||
|
ctx.response.status = HttpStatus.UNAUTHORIZED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string token = generateAccessToken(optionalUser.value);
|
||||||
|
ctx.response.status = HttpStatus.OK;
|
||||||
|
ctx.response.writeBodyString(TokenResponse(token).toJson(), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void postRegister(ref HttpRequestContext ctx) {
|
||||||
|
RegistrationData registrationData;
|
||||||
|
try {
|
||||||
|
registrationData = RegistrationData.parse(ctx.request.readBodyAsJson());
|
||||||
|
} catch (Exception e) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validateUsername(registrationData.username)) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid username.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!validatePassword(registrationData.password)) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UserRepository userRepo = new FileSystemUserRepository();
|
||||||
|
if (!userRepo.findByUsername(registrationData.username).isNull) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Username is taken.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
import botan.passhash.bcrypt : generateBcrypt;
|
||||||
|
import botan.rng.auto_rng;
|
||||||
|
RandomNumberGenerator rng = new AutoSeededRNG();
|
||||||
|
string passwordHash = generateBcrypt(registrationData.password, rng, 12);
|
||||||
|
userRepo.createUser(registrationData.username, passwordHash);
|
||||||
|
infoF!"Created user: %s"(registrationData.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
void getMyUser(ref HttpRequestContext ctx) {
|
||||||
|
AuthContext auth = getAuthContext(ctx);
|
||||||
|
ctx.response.writeBodyString(auth.user.username);
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
module auth.dao;
|
||||||
|
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
import auth.model;
|
||||||
|
|
||||||
|
interface UserRepository {
|
||||||
|
Optional!User findByUsername(string username);
|
||||||
|
User createUser(string username, string passwordHash);
|
||||||
|
void deleteByUsername(string username);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User implementation that stores each user's data in a separate directory.
|
||||||
|
*/
|
||||||
|
class FileSystemUserRepository : UserRepository {
|
||||||
|
import std.path;
|
||||||
|
import std.file;
|
||||||
|
import std.json;
|
||||||
|
|
||||||
|
private string usersDir;
|
||||||
|
|
||||||
|
this(string usersDir = "users") {
|
||||||
|
this.usersDir = usersDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional!User findByUsername(string username) {
|
||||||
|
if (
|
||||||
|
!validateUsername(username) ||
|
||||||
|
!exists(getUserDir(username)) ||
|
||||||
|
!exists(getUserDataFile(username))
|
||||||
|
) {
|
||||||
|
return Optional!User.empty;
|
||||||
|
}
|
||||||
|
JSONValue userObj = parseJSON(readText(getUserDataFile(username)));
|
||||||
|
return Optional!User.of(User(
|
||||||
|
username,
|
||||||
|
userObj.object["passwordHash"].str
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
User createUser(string username, string passwordHash) {
|
||||||
|
if (!validateUsername(username)) throw new Exception("Invalid username");
|
||||||
|
if (exists(getUserDir(username))) throw new Exception("User already exists.");
|
||||||
|
JSONValue userObj = JSONValue.emptyObject;
|
||||||
|
userObj.object["passwordHash"] = JSONValue(passwordHash);
|
||||||
|
string jsonStr = userObj.toPrettyString();
|
||||||
|
if (!exists(this.usersDir)) mkdir(this.usersDir);
|
||||||
|
mkdir(getUserDir(username));
|
||||||
|
std.file.write(getUserDataFile(username), cast(ubyte[]) jsonStr);
|
||||||
|
return User(username, passwordHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteByUsername(string username) {
|
||||||
|
if (validateUsername(username) && exists(getUserDir(username))) {
|
||||||
|
rmdirRecurse(getUserDir(username));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getUserDir(string username) {
|
||||||
|
return buildPath(this.usersDir, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getUserDataFile(string username) {
|
||||||
|
return buildPath(this.usersDir, username, "user-data.json");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/// Defines data-transfer objects for the API's authentication mechanisms.
|
||||||
|
module auth.dto;
|
||||||
|
|
||||||
|
import handy_httpd;
|
||||||
|
import std.json;
|
||||||
|
|
||||||
|
struct LoginCredentials {
|
||||||
|
string username;
|
||||||
|
string password;
|
||||||
|
|
||||||
|
static LoginCredentials parse(JSONValue obj) {
|
||||||
|
if (
|
||||||
|
obj.type != JSONType.OBJECT ||
|
||||||
|
"username" !in obj.object ||
|
||||||
|
"password" !in obj.object ||
|
||||||
|
obj.object["username"].type != JSONType.STRING ||
|
||||||
|
obj.object["password"].type != JSONType.STRING
|
||||||
|
) {
|
||||||
|
throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Malformed login credentials.");
|
||||||
|
}
|
||||||
|
return LoginCredentials(
|
||||||
|
obj.object["username"].str,
|
||||||
|
obj.object["password"].str
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TokenResponse {
|
||||||
|
string token;
|
||||||
|
|
||||||
|
string toJson() {
|
||||||
|
JSONValue obj = JSONValue.emptyObject;
|
||||||
|
obj.object["token"] = JSONValue(token);
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RegistrationData {
|
||||||
|
string username;
|
||||||
|
string password;
|
||||||
|
|
||||||
|
static RegistrationData parse(JSONValue obj) {
|
||||||
|
LoginCredentials lc = LoginCredentials.parse(obj);
|
||||||
|
return RegistrationData(lc.username, lc.password);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Defines models for the Finnow API's authentication system.
|
||||||
|
*/
|
||||||
|
module auth.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user is the basic authenticated entity representing someone who has one
|
||||||
|
* or more profiles.
|
||||||
|
*/
|
||||||
|
struct User {
|
||||||
|
const string username;
|
||||||
|
const string passwordHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a username string.
|
||||||
|
* Params:
|
||||||
|
* username = The username to check.
|
||||||
|
* Returns: True if the username is valid.
|
||||||
|
*/
|
||||||
|
bool validateUsername(string username) {
|
||||||
|
import std.regex;
|
||||||
|
import std.uni : toLower;
|
||||||
|
if (username is null || username.length < 3) return false;
|
||||||
|
const string[] RESERVED_USERNAMES = ["user", "admin"];
|
||||||
|
static foreach (reservedUsername; RESERVED_USERNAMES) {
|
||||||
|
if (toLower(username) == reservedUsername) return false;
|
||||||
|
}
|
||||||
|
auto r = ctRegex!(`^[a-zA-Z]+[a-zA-Z0-9_]+$`);
|
||||||
|
return !matchFirst(username, r).empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a password is of sufficient complexity.
|
||||||
|
* Params:
|
||||||
|
* password = The password to check.
|
||||||
|
* Returns: True if the password is sufficiently complex.
|
||||||
|
*/
|
||||||
|
bool validatePassword(string password) {
|
||||||
|
return password !is null && password.length >= 8;
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
module auth.service;
|
||||||
|
|
||||||
|
import handy_httpd;
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
import slf4d;
|
||||||
|
|
||||||
|
import auth.model;
|
||||||
|
import auth.dao;
|
||||||
|
import handy_httpd.handlers.filtered_handler;
|
||||||
|
|
||||||
|
const SECRET = "temporary-insecure-secret"; // TODO: Load secret from application config!
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a new JWT access token for a user.
|
||||||
|
* Params:
|
||||||
|
* user = The user to generate the token for.
|
||||||
|
* Returns: The token.
|
||||||
|
*/
|
||||||
|
string generateAccessToken(User user) {
|
||||||
|
import jwt.jwt : Token;
|
||||||
|
import jwt.algorithms : JWTAlgorithm;
|
||||||
|
import std.datetime;
|
||||||
|
const TIMEOUT_MINUTES = 30;
|
||||||
|
Token token = new Token(JWTAlgorithm.HS512);
|
||||||
|
token.claims.aud("finnow-api");
|
||||||
|
token.claims.sub(user.username);
|
||||||
|
token.claims.exp(Clock.currTime().toUnixTime() + TIMEOUT_MINUTES * 60);
|
||||||
|
token.claims.iss("finnow-api");
|
||||||
|
return token.encode(SECRET);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request filter that only permits authenticated requests to be processed.
|
||||||
|
*/
|
||||||
|
class TokenAuthenticationFilter : HttpRequestFilter {
|
||||||
|
private static const AUTH_METADATA_KEY = "AuthContext";
|
||||||
|
private immutable string secret;
|
||||||
|
|
||||||
|
this(string secret) {
|
||||||
|
this.secret = secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void apply(ref HttpRequestContext ctx, FilterChain fc) {
|
||||||
|
Optional!AuthContext optionalAuth = validateAuthContext(ctx);
|
||||||
|
if (!optionalAuth.isNull) {
|
||||||
|
ctx.metadata[AUTH_METADATA_KEY] = optionalAuth.value;
|
||||||
|
fc.doFilter(ctx); // Only continue the filter chain if a valid auth context was obtained.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about the current request's authentication status.
|
||||||
|
class AuthContext {
|
||||||
|
string token;
|
||||||
|
User user;
|
||||||
|
this(string token, User user) {
|
||||||
|
this.token = token;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get the authentication context from a request context
|
||||||
|
* that was previously passed through this filter.
|
||||||
|
* Params:
|
||||||
|
* ctx = The request context to get.
|
||||||
|
* Returns: The auth context that has been set.
|
||||||
|
*/
|
||||||
|
AuthContext getAuthContext(ref HttpRequestContext ctx) {
|
||||||
|
return cast(AuthContext) ctx.metadata[TokenAuthenticationFilter.AUTH_METADATA_KEY];
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional!AuthContext validateAuthContext(ref HttpRequestContext ctx) {
|
||||||
|
import jwt.jwt : verify, Token;
|
||||||
|
import jwt.algorithms : JWTAlgorithm;
|
||||||
|
import std.typecons;
|
||||||
|
|
||||||
|
const HEADER_NAME = "Authorization";
|
||||||
|
if (!ctx.request.headers.contains(HEADER_NAME)) {
|
||||||
|
return setUnauthorized(ctx, "Missing Authorization header.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string authorizationHeader = ctx.request.headers.getFirst(HEADER_NAME).orElse("");
|
||||||
|
if (authorizationHeader.length < 7 || authorizationHeader[0..7] != "Bearer ") {
|
||||||
|
return setUnauthorized(ctx, "Invalid Authorization header format. Expected bearer token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string rawToken = authorizationHeader[7..$];
|
||||||
|
try {
|
||||||
|
Token token = verify(rawToken, SECRET, [JWTAlgorithm.HS512]);
|
||||||
|
string username = token.claims.sub;
|
||||||
|
UserRepository userRepo = new FileSystemUserRepository();
|
||||||
|
Optional!User optionalUser = userRepo.findByUsername(username);
|
||||||
|
if (optionalUser.isNull) {
|
||||||
|
return setUnauthorized(ctx, "User does not exist.");
|
||||||
|
}
|
||||||
|
return Optional!AuthContext.of(new AuthContext(rawToken, optionalUser.value));
|
||||||
|
} catch (Exception e) {
|
||||||
|
warn("Failed to verify user token.", e);
|
||||||
|
return setUnauthorized(ctx, "Invalid or malformed token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional!AuthContext setUnauthorized(ref HttpRequestContext ctx, string msg) {
|
||||||
|
ctx.response.status = HttpStatus.UNAUTHORIZED;
|
||||||
|
ctx.response.writeBodyString(msg);
|
||||||
|
return Optional!AuthContext.empty;
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
module data.account;
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
module data.base;
|
||||||
|
|
||||||
|
import model;
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
|
||||||
|
interface DataSource {
|
||||||
|
PropertiesRepository getPropertiesRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropertiesRepository {
|
||||||
|
Optional!string findProperty(string propertyName);
|
||||||
|
void setProperty(string name, string value);
|
||||||
|
void deleteProperty(string name);
|
||||||
|
ProfileProperty[] findAll();
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
module data;
|
||||||
|
|
||||||
|
public import data.base;
|
||||||
|
|
||||||
|
// Utility imports for items commonly used alongside data.
|
||||||
|
public import handy_httpd.components.optional;
|
|
@ -0,0 +1,82 @@
|
||||||
|
module data.sqlite;
|
||||||
|
|
||||||
|
import d2sqlite3;
|
||||||
|
import slf4d;
|
||||||
|
import data.base;
|
||||||
|
import model;
|
||||||
|
|
||||||
|
class SqliteDataSource : DataSource {
|
||||||
|
const SCHEMA = import("schema.sql");
|
||||||
|
|
||||||
|
private const string dbPath;
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
this(string path) {
|
||||||
|
this.dbPath = path;
|
||||||
|
import std.file : exists;
|
||||||
|
bool needsInit = !exists(path);
|
||||||
|
this.db = Database(path);
|
||||||
|
if (needsInit) {
|
||||||
|
infoF!"Initializing database: %s"(dbPath);
|
||||||
|
db.run(SCHEMA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertiesRepository getPropertiesRepository() {
|
||||||
|
return new SqlitePropertiesRepository(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqliteRepository {
|
||||||
|
private Database db;
|
||||||
|
this(Database db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqlitePropertiesRepository : SqliteRepository, PropertiesRepository {
|
||||||
|
this(Database db) {
|
||||||
|
super(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional!string findProperty(string propertyName) {
|
||||||
|
Statement stmt = this.db.prepare("SELECT value FROM profile_property WHERE property = ?");
|
||||||
|
stmt.bind(1, propertyName);
|
||||||
|
ResultRange result = stmt.execute();
|
||||||
|
if (result.empty) return Optional!string.empty;
|
||||||
|
return Optional!string.of(result.front.peek!string(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setProperty(string name, string value) {
|
||||||
|
if (findProperty(name).isNull) {
|
||||||
|
Statement stmt = this.db.prepare("INSERT INTO profile_property (property, value) VALUES (?, ?)");
|
||||||
|
stmt.bind(1, name);
|
||||||
|
stmt.bind(2, value);
|
||||||
|
stmt.execute();
|
||||||
|
} else {
|
||||||
|
Statement stmt = this.db.prepare("UPDATE profile_property SET value = ? WHERE property = ?");
|
||||||
|
stmt.bind(1, value);
|
||||||
|
stmt.bind(2, name);
|
||||||
|
stmt.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteProperty(string name) {
|
||||||
|
Statement stmt = this.db.prepare("DELETE FROM profile_property WHERE property = ?");
|
||||||
|
stmt.bind(1, name);
|
||||||
|
stmt.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileProperty[] findAll() {
|
||||||
|
Statement stmt = this.db.prepare("SELECT * FROM profile_property ORDER BY property ASC");
|
||||||
|
ResultRange result = stmt.execute();
|
||||||
|
ProfileProperty[] props;
|
||||||
|
foreach (Row row; result) {
|
||||||
|
ProfileProperty prop;
|
||||||
|
prop.property = row.peek!string("property");
|
||||||
|
prop.value = row.peek!string("value");
|
||||||
|
props ~= prop;
|
||||||
|
}
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
module model.account;
|
||||||
|
|
||||||
|
import model.base;
|
||||||
|
import std.datetime;
|
||||||
|
|
||||||
|
struct Account {
|
||||||
|
ulong id;
|
||||||
|
SysTime createdAt;
|
||||||
|
bool archived;
|
||||||
|
string type;
|
||||||
|
string numberSuffix;
|
||||||
|
string name;
|
||||||
|
string currency;
|
||||||
|
string description;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AccountCreditCardProperties {
|
||||||
|
ulong account_id;
|
||||||
|
string creditLimit;
|
||||||
|
}
|
|
@ -1,37 +1,17 @@
|
||||||
module model.base;
|
module model.base;
|
||||||
|
|
||||||
/**
|
import std.datetime;
|
||||||
* The base class for all persistent entities with a unique integer id.
|
|
||||||
* It offers some basic utilities like equality and comparison by default.
|
|
||||||
*/
|
|
||||||
abstract class IdEntity {
|
|
||||||
const ulong id;
|
|
||||||
|
|
||||||
this(ulong id) {
|
struct ProfileProperty {
|
||||||
this.id = id;
|
string property;
|
||||||
}
|
string value;
|
||||||
|
}
|
||||||
|
|
||||||
override bool opEquals(Object other) const {
|
struct Attachment {
|
||||||
if (IdEntity e = cast(IdEntity) other) {
|
ulong id;
|
||||||
return e.id == id;
|
SysTime uploadedAt;
|
||||||
}
|
string filename;
|
||||||
return false;
|
string contentType;
|
||||||
}
|
ulong size;
|
||||||
|
ubyte[] content;
|
||||||
override size_t toHash() const {
|
}
|
||||||
import std.conv : to;
|
|
||||||
return id.to!size_t;
|
|
||||||
}
|
|
||||||
|
|
||||||
override string toString() const {
|
|
||||||
import std.format : format;
|
|
||||||
return format!"IdEntity(id = %d)"(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
override int opCmp(Object other) const {
|
|
||||||
if (IdEntity e = cast(IdEntity) other) {
|
|
||||||
return this.id < e.id;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
module model.currency;
|
||||||
|
|
||||||
|
struct Currency {
|
||||||
|
const char[3] code;
|
||||||
|
const ubyte fractionalDigits;
|
||||||
|
const ushort numericCode;
|
||||||
|
|
||||||
|
import std.traits : isSomeString, EnumMembers;
|
||||||
|
static Currency ofCode(S)(S code) if (isSomeString!S) {
|
||||||
|
if (code.length != 3) {
|
||||||
|
throw new Exception("Invalid currency code: " ~ code);
|
||||||
|
}
|
||||||
|
static foreach (c; EnumMembers!Currencies) {
|
||||||
|
if (c.code == code) return c;
|
||||||
|
}
|
||||||
|
throw new Exception("Unknown currency code: " ~ code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Currencies : Currency {
|
||||||
|
USD = Currency("USD", 2, 840),
|
||||||
|
CAD = Currency("CAD", 2, 124),
|
||||||
|
GBP = Currency("GBP", 2, 826),
|
||||||
|
EUR = Currency("EUR", 2, 978),
|
||||||
|
CHF = Currency("CHF", 2, 756),
|
||||||
|
ZAR = Currency("ZAR", 2, 710),
|
||||||
|
JPY = Currency("JPY", 0, 392),
|
||||||
|
INR = Currency("INR", 2, 356)
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
assert(Currency.ofCode("USD") == Currencies.USD);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
module model;
|
||||||
|
|
||||||
|
public import model.base;
|
||||||
|
public import model.account;
|
||||||
|
public import model.transaction;
|
||||||
|
|
||||||
|
// Additional utility imports used often alongside models.
|
||||||
|
public import handy_httpd.components.optional;
|
|
@ -0,0 +1,33 @@
|
||||||
|
module model.transaction;
|
||||||
|
|
||||||
|
import std.datetime;
|
||||||
|
import model.currency;
|
||||||
|
|
||||||
|
struct TransactionVendor {
|
||||||
|
ulong id;
|
||||||
|
string name;
|
||||||
|
string description;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransactionCategory {
|
||||||
|
ulong id;
|
||||||
|
ulong parentId;
|
||||||
|
string name;
|
||||||
|
string color;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TransactionTag {
|
||||||
|
ulong id;
|
||||||
|
string name;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Transaction {
|
||||||
|
ulong id;
|
||||||
|
SysTime timestamp;
|
||||||
|
SysTime addedAt;
|
||||||
|
string amount;
|
||||||
|
Currency currency;
|
||||||
|
string description;
|
||||||
|
ulong vendorId;
|
||||||
|
ulong categoryId;
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* This module contains everything related to a user's Profiles, including
|
||||||
|
* data repositories, API endpoints, models, and logic.
|
||||||
|
*/
|
||||||
|
module profile;
|
||||||
|
|
||||||
|
import handy_httpd;
|
||||||
|
import handy_httpd.components.optional;
|
||||||
|
import auth.model : User;
|
||||||
|
import auth.service : AuthContext, getAuthContext;
|
||||||
|
import data;
|
||||||
|
import model;
|
||||||
|
|
||||||
|
import std.json;
|
||||||
|
import asdf;
|
||||||
|
|
||||||
|
const DEFAULT_USERS_DIR = "users";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A profile is a complete set of Finnow financial data, all stored in one
|
||||||
|
* single database file. The profile's name is used to lookup the database
|
||||||
|
* partition for its data. A user may own multiple profiles.
|
||||||
|
*/
|
||||||
|
class Profile {
|
||||||
|
string name;
|
||||||
|
|
||||||
|
this(string name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
override int opCmp(Object other) const {
|
||||||
|
if (Profile p = cast(Profile) other) {
|
||||||
|
return this.name < p.name;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a profile name.
|
||||||
|
* Params:
|
||||||
|
* name = The name to check.
|
||||||
|
* Returns: True if the profile name is valid.
|
||||||
|
*/
|
||||||
|
bool validateProfileName(string name) {
|
||||||
|
import std.regex;
|
||||||
|
import std.uni : toLower;
|
||||||
|
if (name is null || name.length < 3) return false;
|
||||||
|
auto r = ctRegex!(`^[a-zA-Z]+[a-zA-Z0-9_]+$`);
|
||||||
|
return !matchFirst(name, r).empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileRepository {
|
||||||
|
Optional!Profile findByName(string name);
|
||||||
|
Profile createProfile(string name);
|
||||||
|
Profile[] findAll();
|
||||||
|
void deleteByName(string name);
|
||||||
|
DataSource getDataSource(in Profile profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileSystemProfileRepository : ProfileRepository {
|
||||||
|
import std.path;
|
||||||
|
import std.file;
|
||||||
|
import data;
|
||||||
|
import data.sqlite;
|
||||||
|
|
||||||
|
private const string usersDir;
|
||||||
|
private const string username;
|
||||||
|
|
||||||
|
this(string usersDir, string username) {
|
||||||
|
this.usersDir = usersDir;
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
this(string username) {
|
||||||
|
this(DEFAULT_USERS_DIR, username);
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional!Profile findByName(string name) {
|
||||||
|
string path = getProfilePath(name);
|
||||||
|
if (!exists(path)) return Optional!Profile.empty;
|
||||||
|
return Optional!Profile.of(new Profile(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile createProfile(string name) {
|
||||||
|
string path = getProfilePath(name);
|
||||||
|
if (exists(path)) throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Profile already exists.");
|
||||||
|
if (!exists(getProfilesDir())) mkdir(getProfilesDir());
|
||||||
|
DataSource ds = new SqliteDataSource(path);
|
||||||
|
import std.datetime;
|
||||||
|
auto propsRepo = ds.getPropertiesRepository();
|
||||||
|
propsRepo.setProperty("name", name);
|
||||||
|
propsRepo.setProperty("createdAt", Clock.currTime(UTC()).toISOExtString());
|
||||||
|
propsRepo.setProperty("user", username);
|
||||||
|
return new Profile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile[] findAll() {
|
||||||
|
string profilesDir = getProfilesDir();
|
||||||
|
if (!exists(profilesDir)) return [];
|
||||||
|
Profile[] profiles;
|
||||||
|
foreach (DirEntry entry; dirEntries(profilesDir, SpanMode.shallow, false)) {
|
||||||
|
import std.string : endsWith;
|
||||||
|
const suffix = ".sqlite";
|
||||||
|
if (endsWith(entry.name, suffix)) {
|
||||||
|
string profileName = baseName(entry.name, suffix);
|
||||||
|
profiles ~= new Profile(profileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
import std.algorithm.sorting : sort;
|
||||||
|
sort(profiles);
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteByName(string name) {
|
||||||
|
string path = getProfilePath(name);
|
||||||
|
if (exists(path)) {
|
||||||
|
std.file.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DataSource getDataSource(in Profile profile) {
|
||||||
|
return new SqliteDataSource(getProfilePath(profile.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getProfilesDir() {
|
||||||
|
return buildPath(this.usersDir, username, "profiles");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string getProfilePath(string name) {
|
||||||
|
return buildPath(this.usersDir, username, "profiles", name ~ ".sqlite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Endpoints Below Here!
|
||||||
|
|
||||||
|
void handleCreateNewProfile(ref HttpRequestContext ctx) {
|
||||||
|
JSONValue obj = ctx.request.readBodyAsJson();
|
||||||
|
string name = obj.object["name"].str;
|
||||||
|
if (!validateProfileName(name)) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid profile name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthContext auth = getAuthContext(ctx);
|
||||||
|
ProfileRepository profileRepo = new FileSystemProfileRepository(auth.user.username);
|
||||||
|
profileRepo.createProfile(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleGetProfiles(ref HttpRequestContext ctx) {
|
||||||
|
AuthContext auth = getAuthContext(ctx);
|
||||||
|
ProfileRepository profileRepo = new FileSystemProfileRepository(auth.user.username);
|
||||||
|
Profile[] profiles = profileRepo.findAll();
|
||||||
|
ctx.response.writeBodyString(serializeToJson(profiles), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDeleteProfile(ref HttpRequestContext ctx) {
|
||||||
|
string name = ctx.request.getPathParamAs!string("name");
|
||||||
|
if (!validateProfileName(name)) {
|
||||||
|
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||||
|
ctx.response.writeBodyString("Invalid profile name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AuthContext auth = getAuthContext(ctx);
|
||||||
|
ProfileRepository profileRepo = new FileSystemProfileRepository(auth.user.username);
|
||||||
|
profileRepo.deleteByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleGetProperties(ref HttpRequestContext ctx) {
|
||||||
|
ProfileContext profileCtx = getProfileContextOrThrow(ctx);
|
||||||
|
ProfileRepository profileRepo = new FileSystemProfileRepository(profileCtx.user.username);
|
||||||
|
DataSource ds = profileRepo.getDataSource(profileCtx.profile);
|
||||||
|
auto propsRepo = ds.getPropertiesRepository();
|
||||||
|
ProfileProperty[] props = propsRepo.findAll();
|
||||||
|
ctx.response.writeBodyString(serializeToJson(props), "application/json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contextual information that's available when handling requests under a profile.
|
||||||
|
struct ProfileContext {
|
||||||
|
const Profile profile;
|
||||||
|
const User user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to get a profile context from a request context. This will attempt to
|
||||||
|
* extract a "profile" path parameter and the authenticated user, and combine
|
||||||
|
* them into the ProfileContext.
|
||||||
|
* Params:
|
||||||
|
* ctx = The request context to read.
|
||||||
|
* Returns: An optional profile context.
|
||||||
|
*/
|
||||||
|
Optional!ProfileContext getProfileContext(ref HttpRequestContext ctx) {
|
||||||
|
import auth.service : AuthContext, getAuthContext;
|
||||||
|
if ("profile" !in ctx.request.pathParams) return Optional!ProfileContext.empty;
|
||||||
|
string profileName = ctx.request.pathParams["profile"];
|
||||||
|
if (!validateProfileName(profileName)) return Optional!ProfileContext.empty;
|
||||||
|
AuthContext authCtx = getAuthContext(ctx);
|
||||||
|
if (authCtx is null) return Optional!ProfileContext.empty;
|
||||||
|
User user = authCtx.user;
|
||||||
|
ProfileRepository repo = new FileSystemProfileRepository("users", user.username);
|
||||||
|
return repo.findByName(profileName)
|
||||||
|
.mapIfPresent!(p => ProfileContext(p, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to `getProfileContext`, but throws an HttpStatusException with a
|
||||||
|
* 404 NOT FOUND status if no profile context could be obtained.
|
||||||
|
* Params:
|
||||||
|
* ctx = The request context to read.
|
||||||
|
* Returns: The profile context that was obtained.
|
||||||
|
*/
|
||||||
|
ProfileContext getProfileContextOrThrow(ref HttpRequestContext ctx) {
|
||||||
|
return getProfileContext(ctx).orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
||||||
|
}
|
Loading…
Reference in New Issue